mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-29 08:48:02 -05:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab4e9dc72f | ||
|
|
1c58c35bd8 | ||
|
|
d63a5cb504 | ||
|
|
8bd5d41723 | ||
|
|
c12931bc50 | ||
|
|
e9c4251c1c | ||
|
|
cc2be33d6b | ||
|
|
45371e521e | ||
|
|
0ce0f98aa5 | ||
|
|
dff1c9d083 | ||
|
|
b09f683072 | ||
|
|
a8bb0db660 | ||
|
|
af82820a28 | ||
|
|
4372841797 | ||
|
|
5e8c843241 | ||
|
|
7bf3d73ee6 | ||
|
|
7ffc11a738 | ||
|
|
be578e2ed7 | ||
|
|
f415e5edc4 | ||
|
|
13a6e6c3fa | ||
|
|
f5ab7f21ae | ||
|
|
bfb6fffe38 | ||
|
|
4fbec0a43f | ||
|
|
585f5e365b | ||
|
|
3792bdd252 | ||
|
|
eb5d1f3e5b | ||
|
|
54ab82c8dd | ||
|
|
f895bf469b | ||
|
|
dd3209af06 | ||
|
|
b6ba3b50a7 | ||
|
|
b304233062 | ||
|
|
57e4b49bd6 | ||
|
|
e12dd204ed | ||
|
|
3d9d9cbc54 | ||
|
|
0f4ec962ad | ||
|
|
4827866f9a | ||
|
|
3e697d9ed9 | ||
|
|
4431a1a484 | ||
|
|
4d1a9a3f22 | ||
|
|
eb07a080fb |
@@ -10,7 +10,6 @@ describe('OAuth Token API Routes', () => {
|
|||||||
const mockGetUserId = vi.fn()
|
const mockGetUserId = vi.fn()
|
||||||
const mockGetCredential = vi.fn()
|
const mockGetCredential = vi.fn()
|
||||||
const mockRefreshTokenIfNeeded = vi.fn()
|
const mockRefreshTokenIfNeeded = vi.fn()
|
||||||
const mockGetOAuthToken = vi.fn()
|
|
||||||
const mockAuthorizeCredentialUse = vi.fn()
|
const mockAuthorizeCredentialUse = vi.fn()
|
||||||
const mockCheckHybridAuth = vi.fn()
|
const mockCheckHybridAuth = vi.fn()
|
||||||
|
|
||||||
@@ -30,7 +29,6 @@ describe('OAuth Token API Routes', () => {
|
|||||||
getUserId: mockGetUserId,
|
getUserId: mockGetUserId,
|
||||||
getCredential: mockGetCredential,
|
getCredential: mockGetCredential,
|
||||||
refreshTokenIfNeeded: mockRefreshTokenIfNeeded,
|
refreshTokenIfNeeded: mockRefreshTokenIfNeeded,
|
||||||
getOAuthToken: mockGetOAuthToken,
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.doMock('@sim/logger', () => ({
|
vi.doMock('@sim/logger', () => ({
|
||||||
@@ -232,140 +230,6 @@ describe('OAuth Token API Routes', () => {
|
|||||||
expect(response.status).toBe(401)
|
expect(response.status).toBe(401)
|
||||||
expect(data).toHaveProperty('error', 'Failed to refresh access token')
|
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,22 +71,6 @@ export async function POST(request: NextRequest) {
|
|||||||
providerId,
|
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 {
|
try {
|
||||||
const accessToken = await getOAuthToken(credentialAccountUserId, providerId)
|
const accessToken = await getOAuthToken(credentialAccountUserId, providerId)
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
|
|||||||
@@ -26,9 +26,8 @@ vi.mock('@/serializer', () => ({
|
|||||||
Serializer: vi.fn(),
|
Serializer: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/lib/workflows/subblocks', () => ({
|
vi.mock('@/stores/workflows/server-utils', () => ({
|
||||||
mergeSubblockStateWithValues: vi.fn().mockReturnValue({}),
|
mergeSubblockState: vi.fn().mockReturnValue({}),
|
||||||
mergeSubBlockValues: vi.fn().mockReturnValue({}),
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const mockDecryptSecret = vi.fn()
|
const mockDecryptSecret = vi.fn()
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
const logger = createLogger('AsanaAddCommentAPI')
|
const logger = createLogger('AsanaAddCommentAPI')
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
try {
|
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()
|
const { accessToken, taskGid, text } = await request.json()
|
||||||
|
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
const logger = createLogger('AsanaCreateTaskAPI')
|
const logger = createLogger('AsanaCreateTaskAPI')
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
try {
|
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()
|
const { accessToken, workspace, name, notes, assignee, due_on } = await request.json()
|
||||||
|
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
const logger = createLogger('AsanaGetProjectsAPI')
|
const logger = createLogger('AsanaGetProjectsAPI')
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
try {
|
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()
|
const { accessToken, workspace } = await request.json()
|
||||||
|
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
const logger = createLogger('AsanaGetTaskAPI')
|
const logger = createLogger('AsanaGetTaskAPI')
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
try {
|
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()
|
const { accessToken, taskGid, workspace, project, limit } = await request.json()
|
||||||
|
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
const logger = createLogger('AsanaSearchTasksAPI')
|
const logger = createLogger('AsanaSearchTasksAPI')
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
try {
|
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()
|
const { accessToken, workspace, text, assignee, projects, completed } = await request.json()
|
||||||
|
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
const logger = createLogger('AsanaUpdateTaskAPI')
|
const logger = createLogger('AsanaUpdateTaskAPI')
|
||||||
|
|
||||||
export async function PUT(request: NextRequest) {
|
export async function PUT(request: Request) {
|
||||||
try {
|
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()
|
const { accessToken, taskGid, name, notes, assignee, completed, due_on } = await request.json()
|
||||||
|
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||||
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||||
|
|
||||||
@@ -9,13 +8,8 @@ const logger = createLogger('ConfluenceAttachmentAPI')
|
|||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
// Delete an attachment
|
// Delete an attachment
|
||||||
export async function DELETE(request: NextRequest) {
|
export async function DELETE(request: Request) {
|
||||||
try {
|
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()
|
const { domain, accessToken, cloudId: providedCloudId, attachmentId } = await request.json()
|
||||||
|
|
||||||
if (!domain) {
|
if (!domain) {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||||
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||||
|
|
||||||
@@ -9,13 +8,8 @@ const logger = createLogger('ConfluenceAttachmentsAPI')
|
|||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
// List attachments on a page
|
// List attachments on a page
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: Request) {
|
||||||
try {
|
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 { searchParams } = new URL(request.url)
|
||||||
const domain = searchParams.get('domain')
|
const domain = searchParams.get('domain')
|
||||||
const accessToken = searchParams.get('accessToken')
|
const accessToken = searchParams.get('accessToken')
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||||
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||||
|
|
||||||
@@ -47,13 +46,8 @@ const deleteCommentSchema = z
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Update a comment
|
// Update a comment
|
||||||
export async function PUT(request: NextRequest) {
|
export async function PUT(request: Request) {
|
||||||
try {
|
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 body = await request.json()
|
||||||
|
|
||||||
const validation = putCommentSchema.safeParse(body)
|
const validation = putCommentSchema.safeParse(body)
|
||||||
@@ -134,13 +128,8 @@ export async function PUT(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delete a comment
|
// Delete a comment
|
||||||
export async function DELETE(request: NextRequest) {
|
export async function DELETE(request: Request) {
|
||||||
try {
|
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 body = await request.json()
|
||||||
|
|
||||||
const validation = deleteCommentSchema.safeParse(body)
|
const validation = deleteCommentSchema.safeParse(body)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||||
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||||
|
|
||||||
@@ -9,13 +8,8 @@ const logger = createLogger('ConfluenceCommentsAPI')
|
|||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
// Create a comment
|
// Create a comment
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
try {
|
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()
|
const { domain, accessToken, cloudId: providedCloudId, pageId, comment } = await request.json()
|
||||||
|
|
||||||
if (!domain) {
|
if (!domain) {
|
||||||
@@ -92,13 +86,8 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// List comments on a page
|
// List comments on a page
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: Request) {
|
||||||
try {
|
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 { searchParams } = new URL(request.url)
|
||||||
const domain = searchParams.get('domain')
|
const domain = searchParams.get('domain')
|
||||||
const accessToken = searchParams.get('accessToken')
|
const accessToken = searchParams.get('accessToken')
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||||
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||||
|
|
||||||
@@ -8,13 +7,8 @@ const logger = createLogger('ConfluenceCreatePageAPI')
|
|||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const auth = await checkSessionOrInternalAuth(request)
|
|
||||||
if (!auth.success || !auth.userId) {
|
|
||||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
domain,
|
domain,
|
||||||
accessToken,
|
accessToken,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||||
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||||
|
|
||||||
@@ -9,13 +8,8 @@ const logger = createLogger('ConfluenceLabelsAPI')
|
|||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
// Add a label to a page
|
// Add a label to a page
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const auth = await checkSessionOrInternalAuth(request)
|
|
||||||
if (!auth.success || !auth.userId) {
|
|
||||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
domain,
|
domain,
|
||||||
accessToken,
|
accessToken,
|
||||||
@@ -93,13 +87,8 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// List labels on a page
|
// List labels on a page
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: Request) {
|
||||||
try {
|
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 { searchParams } = new URL(request.url)
|
||||||
const domain = searchParams.get('domain')
|
const domain = searchParams.get('domain')
|
||||||
const accessToken = searchParams.get('accessToken')
|
const accessToken = searchParams.get('accessToken')
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||||
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||||
|
|
||||||
@@ -74,13 +73,8 @@ const deletePageSchema = z
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
try {
|
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 body = await request.json()
|
||||||
|
|
||||||
const validation = postPageSchema.safeParse(body)
|
const validation = postPageSchema.safeParse(body)
|
||||||
@@ -150,13 +144,8 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function PUT(request: NextRequest) {
|
export async function PUT(request: Request) {
|
||||||
try {
|
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 body = await request.json()
|
||||||
|
|
||||||
const validation = putPageSchema.safeParse(body)
|
const validation = putPageSchema.safeParse(body)
|
||||||
@@ -259,13 +248,8 @@ export async function PUT(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function DELETE(request: NextRequest) {
|
export async function DELETE(request: Request) {
|
||||||
try {
|
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 body = await request.json()
|
||||||
|
|
||||||
const validation = deletePageSchema.safeParse(body)
|
const validation = deletePageSchema.safeParse(body)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
|
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||||
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||||
|
|
||||||
@@ -9,13 +8,8 @@ const logger = createLogger('ConfluencePagesAPI')
|
|||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
// List pages or search pages
|
// List pages or search pages
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const auth = await checkSessionOrInternalAuth(request)
|
|
||||||
if (!auth.success || !auth.userId) {
|
|
||||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
domain,
|
domain,
|
||||||
accessToken,
|
accessToken,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
|
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||||
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||||
|
|
||||||
@@ -8,13 +7,8 @@ export const dynamic = 'force-dynamic'
|
|||||||
|
|
||||||
const logger = createLogger('Confluence Search')
|
const logger = createLogger('Confluence Search')
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const auth = await checkSessionOrInternalAuth(request)
|
|
||||||
if (!auth.success || !auth.userId) {
|
|
||||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
domain,
|
domain,
|
||||||
accessToken,
|
accessToken,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||||
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||||
|
|
||||||
@@ -9,13 +8,8 @@ const logger = createLogger('ConfluenceSpaceAPI')
|
|||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
// Get a specific space
|
// Get a specific space
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: Request) {
|
||||||
try {
|
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 { searchParams } = new URL(request.url)
|
||||||
const domain = searchParams.get('domain')
|
const domain = searchParams.get('domain')
|
||||||
const accessToken = searchParams.get('accessToken')
|
const accessToken = searchParams.get('accessToken')
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
|
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||||
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||||
|
|
||||||
@@ -9,13 +8,8 @@ const logger = createLogger('ConfluenceSpacesAPI')
|
|||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
// List all spaces
|
// List all spaces
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: Request) {
|
||||||
try {
|
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 { searchParams } = new URL(request.url)
|
||||||
const domain = searchParams.get('domain')
|
const domain = searchParams.get('domain')
|
||||||
const accessToken = searchParams.get('accessToken')
|
const accessToken = searchParams.get('accessToken')
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, 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 { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||||
import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils'
|
import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils'
|
||||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||||
@@ -12,11 +11,6 @@ export const dynamic = 'force-dynamic'
|
|||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
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 body = await request.json()
|
||||||
const { domain, accessToken, cloudId: providedCloudId, pageId, file, fileName, comment } = body
|
const { domain, accessToken, cloudId: providedCloudId, pageId, file, fileName, comment } = body
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateNumericId } from '@/lib/core/security/input-validation'
|
import { validateNumericId } from '@/lib/core/security/input-validation'
|
||||||
|
|
||||||
interface DiscordChannel {
|
interface DiscordChannel {
|
||||||
@@ -14,12 +13,7 @@ export const dynamic = 'force-dynamic'
|
|||||||
|
|
||||||
const logger = createLogger('DiscordChannelsAPI')
|
const logger = createLogger('DiscordChannelsAPI')
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
const auth = await checkInternalAuth(request)
|
|
||||||
if (!auth.success || !auth.userId) {
|
|
||||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { botToken, serverId, channelId } = await request.json()
|
const { botToken, serverId, channelId } = await request.json()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateNumericId } from '@/lib/core/security/input-validation'
|
import { validateNumericId } from '@/lib/core/security/input-validation'
|
||||||
|
|
||||||
interface DiscordServer {
|
interface DiscordServer {
|
||||||
@@ -13,12 +12,7 @@ export const dynamic = 'force-dynamic'
|
|||||||
|
|
||||||
const logger = createLogger('DiscordServersAPI')
|
const logger = createLogger('DiscordServersAPI')
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
const auth = await checkInternalAuth(request)
|
|
||||||
if (!auth.success || !auth.userId) {
|
|
||||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { botToken, serverId } = await request.json()
|
const { botToken, serverId } = await request.json()
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||||
@@ -16,11 +15,6 @@ export async function GET(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
logger.info(`[${requestId}] Google Drive file request received`)
|
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 {
|
try {
|
||||||
const { searchParams } = new URL(request.url)
|
const { searchParams } = new URL(request.url)
|
||||||
const credentialId = searchParams.get('credentialId')
|
const credentialId = searchParams.get('credentialId')
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { getSession } from '@/lib/auth'
|
||||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||||
@@ -73,12 +73,14 @@ export async function GET(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
logger.info(`[${requestId}] Google Drive files request received`)
|
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 {
|
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 { searchParams } = new URL(request.url)
|
||||||
const credentialId = searchParams.get('credentialId')
|
const credentialId = searchParams.get('credentialId')
|
||||||
const mimeType = searchParams.get('mimeType')
|
const mimeType = searchParams.get('mimeType')
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { createDynamoDBClient, deleteItem } from '@/app/api/tools/dynamodb/utils'
|
import { createDynamoDBClient, deleteItem } from '@/app/api/tools/dynamodb/utils'
|
||||||
|
|
||||||
const DeleteSchema = z.object({
|
const DeleteSchema = z.object({
|
||||||
@@ -14,13 +13,8 @@ const DeleteSchema = z.object({
|
|||||||
conditionExpression: z.string().optional(),
|
conditionExpression: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
try {
|
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 body = await request.json()
|
||||||
const validatedData = DeleteSchema.parse(body)
|
const validatedData = DeleteSchema.parse(body)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { createDynamoDBClient, getItem } from '@/app/api/tools/dynamodb/utils'
|
import { createDynamoDBClient, getItem } from '@/app/api/tools/dynamodb/utils'
|
||||||
|
|
||||||
const GetSchema = z.object({
|
const GetSchema = z.object({
|
||||||
@@ -20,13 +19,8 @@ const GetSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
try {
|
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 body = await request.json()
|
||||||
const validatedData = GetSchema.parse(body)
|
const validatedData = GetSchema.parse(body)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { createRawDynamoDBClient, describeTable, listTables } from '@/app/api/tools/dynamodb/utils'
|
import { createRawDynamoDBClient, describeTable, listTables } from '@/app/api/tools/dynamodb/utils'
|
||||||
|
|
||||||
const logger = createLogger('DynamoDBIntrospectAPI')
|
const logger = createLogger('DynamoDBIntrospectAPI')
|
||||||
@@ -18,11 +17,6 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
|
|
||||||
try {
|
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 body = await request.json()
|
||||||
const params = IntrospectSchema.parse(body)
|
const params = IntrospectSchema.parse(body)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { createDynamoDBClient, putItem } from '@/app/api/tools/dynamodb/utils'
|
import { createDynamoDBClient, putItem } from '@/app/api/tools/dynamodb/utils'
|
||||||
|
|
||||||
const PutSchema = z.object({
|
const PutSchema = z.object({
|
||||||
@@ -13,13 +12,8 @@ const PutSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
try {
|
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 body = await request.json()
|
||||||
const validatedData = PutSchema.parse(body)
|
const validatedData = PutSchema.parse(body)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { createDynamoDBClient, queryItems } from '@/app/api/tools/dynamodb/utils'
|
import { createDynamoDBClient, queryItems } from '@/app/api/tools/dynamodb/utils'
|
||||||
|
|
||||||
const QuerySchema = z.object({
|
const QuerySchema = z.object({
|
||||||
@@ -16,13 +15,8 @@ const QuerySchema = z.object({
|
|||||||
limit: z.number().positive().optional(),
|
limit: z.number().positive().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
try {
|
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 body = await request.json()
|
||||||
const validatedData = QuerySchema.parse(body)
|
const validatedData = QuerySchema.parse(body)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { createDynamoDBClient, scanItems } from '@/app/api/tools/dynamodb/utils'
|
import { createDynamoDBClient, scanItems } from '@/app/api/tools/dynamodb/utils'
|
||||||
|
|
||||||
const ScanSchema = z.object({
|
const ScanSchema = z.object({
|
||||||
@@ -15,13 +14,8 @@ const ScanSchema = z.object({
|
|||||||
limit: z.number().positive().optional(),
|
limit: z.number().positive().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
try {
|
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 body = await request.json()
|
||||||
const validatedData = ScanSchema.parse(body)
|
const validatedData = ScanSchema.parse(body)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { createDynamoDBClient, updateItem } from '@/app/api/tools/dynamodb/utils'
|
import { createDynamoDBClient, updateItem } from '@/app/api/tools/dynamodb/utils'
|
||||||
|
|
||||||
const UpdateSchema = z.object({
|
const UpdateSchema = z.object({
|
||||||
@@ -17,13 +16,8 @@ const UpdateSchema = z.object({
|
|||||||
conditionExpression: z.string().optional(),
|
conditionExpression: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
try {
|
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 body = await request.json()
|
||||||
const validatedData = UpdateSchema.parse(body)
|
const validatedData = UpdateSchema.parse(body)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||||
|
|
||||||
@@ -30,11 +29,6 @@ export async function GET(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
logger.info(`[${requestId}] Google Sheets sheets request received`)
|
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 {
|
try {
|
||||||
const { searchParams } = new URL(request.url)
|
const { searchParams } = new URL(request.url)
|
||||||
const credentialId = searchParams.get('credentialId')
|
const credentialId = searchParams.get('credentialId')
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||||
import { getJiraCloudId } from '@/tools/jira/utils'
|
import { getJiraCloudId } from '@/tools/jira/utils'
|
||||||
|
|
||||||
@@ -8,13 +7,8 @@ export const dynamic = 'force-dynamic'
|
|||||||
|
|
||||||
const logger = createLogger('JiraIssueAPI')
|
const logger = createLogger('JiraIssueAPI')
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
try {
|
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()
|
const { domain, accessToken, issueId, cloudId: providedCloudId } = await request.json()
|
||||||
if (!domain) {
|
if (!domain) {
|
||||||
logger.error('Missing domain in request')
|
logger.error('Missing domain in request')
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||||
import { getJiraCloudId } from '@/tools/jira/utils'
|
import { getJiraCloudId } from '@/tools/jira/utils'
|
||||||
|
|
||||||
@@ -27,13 +26,8 @@ const validateRequiredParams = (domain: string | null, accessToken: string | nul
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
try {
|
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 { domain, accessToken, issueKeys = [], cloudId: providedCloudId } = await request.json()
|
||||||
|
|
||||||
const validationError = validateRequiredParams(domain || null, accessToken || null)
|
const validationError = validateRequiredParams(domain || null, accessToken || null)
|
||||||
@@ -107,13 +101,8 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: Request) {
|
||||||
try {
|
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 url = new URL(request.url)
|
||||||
const domain = url.searchParams.get('domain')?.trim()
|
const domain = url.searchParams.get('domain')?.trim()
|
||||||
const accessToken = url.searchParams.get('accessToken')
|
const accessToken = url.searchParams.get('accessToken')
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||||
import { getJiraCloudId } from '@/tools/jira/utils'
|
import { getJiraCloudId } from '@/tools/jira/utils'
|
||||||
|
|
||||||
@@ -8,13 +7,8 @@ export const dynamic = 'force-dynamic'
|
|||||||
|
|
||||||
const logger = createLogger('JiraProjectsAPI')
|
const logger = createLogger('JiraProjectsAPI')
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: Request) {
|
||||||
try {
|
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 url = new URL(request.url)
|
||||||
const domain = url.searchParams.get('domain')?.trim()
|
const domain = url.searchParams.get('domain')?.trim()
|
||||||
const accessToken = url.searchParams.get('accessToken')
|
const accessToken = url.searchParams.get('accessToken')
|
||||||
@@ -104,13 +98,8 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
try {
|
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()
|
const { domain, accessToken, projectId, cloudId: providedCloudId } = await request.json()
|
||||||
|
|
||||||
if (!domain) {
|
if (!domain) {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||||
import { getJiraCloudId } from '@/tools/jira/utils'
|
import { getJiraCloudId } from '@/tools/jira/utils'
|
||||||
|
|
||||||
@@ -22,13 +21,8 @@ const jiraUpdateSchema = z.object({
|
|||||||
cloudId: z.string().optional(),
|
cloudId: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function PUT(request: NextRequest) {
|
export async function PUT(request: Request) {
|
||||||
try {
|
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 body = await request.json()
|
||||||
const validation = jiraUpdateSchema.safeParse(body)
|
const validation = jiraUpdateSchema.safeParse(body)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||||
import { getJiraCloudId } from '@/tools/jira/utils'
|
import { getJiraCloudId } from '@/tools/jira/utils'
|
||||||
|
|
||||||
@@ -8,13 +7,8 @@ export const dynamic = 'force-dynamic'
|
|||||||
|
|
||||||
const logger = createLogger('JiraWriteAPI')
|
const logger = createLogger('JiraWriteAPI')
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const auth = await checkSessionOrInternalAuth(request)
|
|
||||||
if (!auth.success || !auth.userId) {
|
|
||||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
domain,
|
domain,
|
||||||
accessToken,
|
accessToken,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import {
|
import {
|
||||||
validateAlphanumericId,
|
validateAlphanumericId,
|
||||||
validateEnum,
|
validateEnum,
|
||||||
@@ -16,12 +15,7 @@ const logger = createLogger('JsmApprovalsAPI')
|
|||||||
const VALID_ACTIONS = ['get', 'answer'] as const
|
const VALID_ACTIONS = ['get', 'answer'] as const
|
||||||
const VALID_DECISIONS = ['approve', 'decline'] as const
|
const VALID_DECISIONS = ['approve', 'decline'] as const
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
const auth = await checkInternalAuth(request)
|
|
||||||
if (!auth.success || !auth.userId) {
|
|
||||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||||
|
|
||||||
@@ -8,12 +7,7 @@ export const dynamic = 'force-dynamic'
|
|||||||
|
|
||||||
const logger = createLogger('JsmCommentAPI')
|
const logger = createLogger('JsmCommentAPI')
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
const auth = await checkInternalAuth(request)
|
|
||||||
if (!auth.success || !auth.userId) {
|
|
||||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
domain,
|
domain,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||||
|
|
||||||
@@ -8,12 +7,7 @@ export const dynamic = 'force-dynamic'
|
|||||||
|
|
||||||
const logger = createLogger('JsmCommentsAPI')
|
const logger = createLogger('JsmCommentsAPI')
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
const auth = await checkInternalAuth(request)
|
|
||||||
if (!auth.success || !auth.userId) {
|
|
||||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||||
|
|
||||||
@@ -8,12 +7,7 @@ export const dynamic = 'force-dynamic'
|
|||||||
|
|
||||||
const logger = createLogger('JsmCustomersAPI')
|
const logger = createLogger('JsmCustomersAPI')
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
const auth = await checkInternalAuth(request)
|
|
||||||
if (!auth.success || !auth.userId) {
|
|
||||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import {
|
import {
|
||||||
validateAlphanumericId,
|
validateAlphanumericId,
|
||||||
validateEnum,
|
validateEnum,
|
||||||
@@ -14,12 +13,7 @@ const logger = createLogger('JsmOrganizationAPI')
|
|||||||
|
|
||||||
const VALID_ACTIONS = ['create', 'add_to_service_desk'] as const
|
const VALID_ACTIONS = ['create', 'add_to_service_desk'] as const
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
const auth = await checkInternalAuth(request)
|
|
||||||
if (!auth.success || !auth.userId) {
|
|
||||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||||
|
|
||||||
@@ -8,12 +7,7 @@ export const dynamic = 'force-dynamic'
|
|||||||
|
|
||||||
const logger = createLogger('JsmOrganizationsAPI')
|
const logger = createLogger('JsmOrganizationsAPI')
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
const auth = await checkInternalAuth(request)
|
|
||||||
if (!auth.success || !auth.userId) {
|
|
||||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const { domain, accessToken, cloudId: cloudIdParam, serviceDeskId, start, limit } = body
|
const { domain, accessToken, cloudId: cloudIdParam, serviceDeskId, start, limit } = body
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import {
|
import {
|
||||||
validateEnum,
|
validateEnum,
|
||||||
validateJiraCloudId,
|
validateJiraCloudId,
|
||||||
@@ -14,12 +13,7 @@ const logger = createLogger('JsmParticipantsAPI')
|
|||||||
|
|
||||||
const VALID_ACTIONS = ['get', 'add'] as const
|
const VALID_ACTIONS = ['get', 'add'] as const
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
const auth = await checkInternalAuth(request)
|
|
||||||
if (!auth.success || !auth.userId) {
|
|
||||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||||
|
|
||||||
@@ -8,12 +7,7 @@ export const dynamic = 'force-dynamic'
|
|||||||
|
|
||||||
const logger = createLogger('JsmQueuesAPI')
|
const logger = createLogger('JsmQueuesAPI')
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
const auth = await checkInternalAuth(request)
|
|
||||||
if (!auth.success || !auth.userId) {
|
|
||||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import {
|
import {
|
||||||
validateAlphanumericId,
|
validateAlphanumericId,
|
||||||
validateJiraCloudId,
|
validateJiraCloudId,
|
||||||
@@ -12,12 +11,7 @@ export const dynamic = 'force-dynamic'
|
|||||||
|
|
||||||
const logger = createLogger('JsmRequestAPI')
|
const logger = createLogger('JsmRequestAPI')
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
const auth = await checkInternalAuth(request)
|
|
||||||
if (!auth.success || !auth.userId) {
|
|
||||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||||
|
|
||||||
@@ -8,12 +7,7 @@ export const dynamic = 'force-dynamic'
|
|||||||
|
|
||||||
const logger = createLogger('JsmRequestsAPI')
|
const logger = createLogger('JsmRequestsAPI')
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
const auth = await checkInternalAuth(request)
|
|
||||||
if (!auth.success || !auth.userId) {
|
|
||||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||||
|
|
||||||
@@ -8,12 +7,7 @@ export const dynamic = 'force-dynamic'
|
|||||||
|
|
||||||
const logger = createLogger('JsmRequestTypesAPI')
|
const logger = createLogger('JsmRequestTypesAPI')
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
const auth = await checkInternalAuth(request)
|
|
||||||
if (!auth.success || !auth.userId) {
|
|
||||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const { domain, accessToken, cloudId: cloudIdParam, serviceDeskId, start, limit } = body
|
const { domain, accessToken, cloudId: cloudIdParam, serviceDeskId, start, limit } = body
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
|
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||||
|
|
||||||
@@ -8,12 +7,7 @@ export const dynamic = 'force-dynamic'
|
|||||||
|
|
||||||
const logger = createLogger('JsmServiceDesksAPI')
|
const logger = createLogger('JsmServiceDesksAPI')
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
const auth = await checkInternalAuth(request)
|
|
||||||
if (!auth.success || !auth.userId) {
|
|
||||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const { domain, accessToken, cloudId: cloudIdParam, start, limit } = body
|
const { domain, accessToken, cloudId: cloudIdParam, start, limit } = body
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||||
|
|
||||||
@@ -8,12 +7,7 @@ export const dynamic = 'force-dynamic'
|
|||||||
|
|
||||||
const logger = createLogger('JsmSlaAPI')
|
const logger = createLogger('JsmSlaAPI')
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
const auth = await checkInternalAuth(request)
|
|
||||||
if (!auth.success || !auth.userId) {
|
|
||||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey, start, limit } = body
|
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey, start, limit } = body
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import {
|
import {
|
||||||
validateAlphanumericId,
|
validateAlphanumericId,
|
||||||
validateJiraCloudId,
|
validateJiraCloudId,
|
||||||
@@ -12,12 +11,7 @@ export const dynamic = 'force-dynamic'
|
|||||||
|
|
||||||
const logger = createLogger('JsmTransitionAPI')
|
const logger = createLogger('JsmTransitionAPI')
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
const auth = await checkInternalAuth(request)
|
|
||||||
if (!auth.success || !auth.userId) {
|
|
||||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
domain,
|
domain,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||||
|
|
||||||
@@ -8,12 +7,7 @@ export const dynamic = 'force-dynamic'
|
|||||||
|
|
||||||
const logger = createLogger('JsmTransitionsAPI')
|
const logger = createLogger('JsmTransitionsAPI')
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: Request) {
|
||||||
const auth = await checkInternalAuth(request)
|
|
||||||
if (!auth.success || !auth.userId) {
|
|
||||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey } = body
|
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey } = body
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { createMongoDBConnection, sanitizeCollectionName, validateFilter } from '../utils'
|
import { createMongoDBConnection, sanitizeCollectionName, validateFilter } from '../utils'
|
||||||
|
|
||||||
const logger = createLogger('MongoDBDeleteAPI')
|
const logger = createLogger('MongoDBDeleteAPI')
|
||||||
@@ -41,12 +40,6 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
let client = null
|
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 {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const params = DeleteSchema.parse(body)
|
const params = DeleteSchema.parse(body)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { createMongoDBConnection, sanitizeCollectionName, validatePipeline } from '../utils'
|
import { createMongoDBConnection, sanitizeCollectionName, validatePipeline } from '../utils'
|
||||||
|
|
||||||
const logger = createLogger('MongoDBExecuteAPI')
|
const logger = createLogger('MongoDBExecuteAPI')
|
||||||
@@ -33,12 +32,6 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
let client = null
|
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 {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const params = ExecuteSchema.parse(body)
|
const params = ExecuteSchema.parse(body)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { createMongoDBConnection, sanitizeCollectionName } from '../utils'
|
import { createMongoDBConnection, sanitizeCollectionName } from '../utils'
|
||||||
|
|
||||||
const logger = createLogger('MongoDBInsertAPI')
|
const logger = createLogger('MongoDBInsertAPI')
|
||||||
@@ -38,12 +37,6 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
let client = null
|
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 {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const params = InsertSchema.parse(body)
|
const params = InsertSchema.parse(body)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { createMongoDBConnection, executeIntrospect } from '../utils'
|
import { createMongoDBConnection, executeIntrospect } from '../utils'
|
||||||
|
|
||||||
const logger = createLogger('MongoDBIntrospectAPI')
|
const logger = createLogger('MongoDBIntrospectAPI')
|
||||||
@@ -21,12 +20,6 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
let client = null
|
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 {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const params = IntrospectSchema.parse(body)
|
const params = IntrospectSchema.parse(body)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { createMongoDBConnection, sanitizeCollectionName, validateFilter } from '../utils'
|
import { createMongoDBConnection, sanitizeCollectionName, validateFilter } from '../utils'
|
||||||
|
|
||||||
const logger = createLogger('MongoDBQueryAPI')
|
const logger = createLogger('MongoDBQueryAPI')
|
||||||
@@ -50,12 +49,6 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
let client = null
|
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 {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const params = QuerySchema.parse(body)
|
const params = QuerySchema.parse(body)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { createMongoDBConnection, sanitizeCollectionName, validateFilter } from '../utils'
|
import { createMongoDBConnection, sanitizeCollectionName, validateFilter } from '../utils'
|
||||||
|
|
||||||
const logger = createLogger('MongoDBUpdateAPI')
|
const logger = createLogger('MongoDBUpdateAPI')
|
||||||
@@ -60,12 +59,6 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = randomUUID().slice(0, 8)
|
const requestId = randomUUID().slice(0, 8)
|
||||||
let client = null
|
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 {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const params = UpdateSchema.parse(body)
|
const params = UpdateSchema.parse(body)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import {
|
import {
|
||||||
convertNeo4jTypesToJSON,
|
convertNeo4jTypesToJSON,
|
||||||
createNeo4jDriver,
|
createNeo4jDriver,
|
||||||
@@ -27,12 +26,6 @@ export async function POST(request: NextRequest) {
|
|||||||
let driver = null
|
let driver = null
|
||||||
let session = 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 {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const params = CreateSchema.parse(body)
|
const params = CreateSchema.parse(body)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { createNeo4jDriver, validateCypherQuery } from '@/app/api/tools/neo4j/utils'
|
import { createNeo4jDriver, validateCypherQuery } from '@/app/api/tools/neo4j/utils'
|
||||||
|
|
||||||
const logger = createLogger('Neo4jDeleteAPI')
|
const logger = createLogger('Neo4jDeleteAPI')
|
||||||
@@ -24,12 +23,6 @@ export async function POST(request: NextRequest) {
|
|||||||
let driver = null
|
let driver = null
|
||||||
let session = 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 {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const params = DeleteSchema.parse(body)
|
const params = DeleteSchema.parse(body)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import {
|
import {
|
||||||
convertNeo4jTypesToJSON,
|
convertNeo4jTypesToJSON,
|
||||||
createNeo4jDriver,
|
createNeo4jDriver,
|
||||||
@@ -27,12 +26,6 @@ export async function POST(request: NextRequest) {
|
|||||||
let driver = null
|
let driver = null
|
||||||
let session = 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 {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const params = ExecuteSchema.parse(body)
|
const params = ExecuteSchema.parse(body)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { createNeo4jDriver } from '@/app/api/tools/neo4j/utils'
|
import { createNeo4jDriver } from '@/app/api/tools/neo4j/utils'
|
||||||
import type { Neo4jNodeSchema, Neo4jRelationshipSchema } from '@/tools/neo4j/types'
|
import type { Neo4jNodeSchema, Neo4jRelationshipSchema } from '@/tools/neo4j/types'
|
||||||
|
|
||||||
@@ -22,12 +21,6 @@ export async function POST(request: NextRequest) {
|
|||||||
let driver = null
|
let driver = null
|
||||||
let session = 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 {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const params = IntrospectSchema.parse(body)
|
const params = IntrospectSchema.parse(body)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import {
|
import {
|
||||||
convertNeo4jTypesToJSON,
|
convertNeo4jTypesToJSON,
|
||||||
createNeo4jDriver,
|
createNeo4jDriver,
|
||||||
@@ -27,12 +26,6 @@ export async function POST(request: NextRequest) {
|
|||||||
let driver = null
|
let driver = null
|
||||||
let session = 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 {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const params = MergeSchema.parse(body)
|
const params = MergeSchema.parse(body)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import {
|
import {
|
||||||
convertNeo4jTypesToJSON,
|
convertNeo4jTypesToJSON,
|
||||||
createNeo4jDriver,
|
createNeo4jDriver,
|
||||||
@@ -27,12 +26,6 @@ export async function POST(request: NextRequest) {
|
|||||||
let driver = null
|
let driver = null
|
||||||
let session = 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 {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const params = QuerySchema.parse(body)
|
const params = QuerySchema.parse(body)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import {
|
import {
|
||||||
convertNeo4jTypesToJSON,
|
convertNeo4jTypesToJSON,
|
||||||
createNeo4jDriver,
|
createNeo4jDriver,
|
||||||
@@ -27,12 +26,6 @@ export async function POST(request: NextRequest) {
|
|||||||
let driver = null
|
let driver = null
|
||||||
let session = 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 {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const params = UpdateSchema.parse(body)
|
const params = UpdateSchema.parse(body)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { createRdsClient, executeDelete } from '@/app/api/tools/rds/utils'
|
import { createRdsClient, executeDelete } from '@/app/api/tools/rds/utils'
|
||||||
|
|
||||||
const logger = createLogger('RDSDeleteAPI')
|
const logger = createLogger('RDSDeleteAPI')
|
||||||
@@ -23,11 +22,6 @@ const DeleteSchema = z.object({
|
|||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
const requestId = randomUUID().slice(0, 8)
|
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 {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const params = DeleteSchema.parse(body)
|
const params = DeleteSchema.parse(body)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { createRdsClient, executeStatement } from '@/app/api/tools/rds/utils'
|
import { createRdsClient, executeStatement } from '@/app/api/tools/rds/utils'
|
||||||
|
|
||||||
const logger = createLogger('RDSExecuteAPI')
|
const logger = createLogger('RDSExecuteAPI')
|
||||||
@@ -20,11 +19,6 @@ const ExecuteSchema = z.object({
|
|||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
const requestId = randomUUID().slice(0, 8)
|
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 {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const params = ExecuteSchema.parse(body)
|
const params = ExecuteSchema.parse(body)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { createRdsClient, executeInsert } from '@/app/api/tools/rds/utils'
|
import { createRdsClient, executeInsert } from '@/app/api/tools/rds/utils'
|
||||||
|
|
||||||
const logger = createLogger('RDSInsertAPI')
|
const logger = createLogger('RDSInsertAPI')
|
||||||
@@ -23,11 +22,6 @@ const InsertSchema = z.object({
|
|||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
const requestId = randomUUID().slice(0, 8)
|
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 {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const params = InsertSchema.parse(body)
|
const params = InsertSchema.parse(body)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { createRdsClient, executeIntrospect, type RdsEngine } from '@/app/api/tools/rds/utils'
|
import { createRdsClient, executeIntrospect, type RdsEngine } from '@/app/api/tools/rds/utils'
|
||||||
|
|
||||||
const logger = createLogger('RDSIntrospectAPI')
|
const logger = createLogger('RDSIntrospectAPI')
|
||||||
@@ -21,11 +20,6 @@ const IntrospectSchema = z.object({
|
|||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
const requestId = randomUUID().slice(0, 8)
|
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 {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const params = IntrospectSchema.parse(body)
|
const params = IntrospectSchema.parse(body)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { createRdsClient, executeStatement, validateQuery } from '@/app/api/tools/rds/utils'
|
import { createRdsClient, executeStatement, validateQuery } from '@/app/api/tools/rds/utils'
|
||||||
|
|
||||||
const logger = createLogger('RDSQueryAPI')
|
const logger = createLogger('RDSQueryAPI')
|
||||||
@@ -20,11 +19,6 @@ const QuerySchema = z.object({
|
|||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
const requestId = randomUUID().slice(0, 8)
|
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 {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const params = QuerySchema.parse(body)
|
const params = QuerySchema.parse(body)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { createRdsClient, executeUpdate } from '@/app/api/tools/rds/utils'
|
import { createRdsClient, executeUpdate } from '@/app/api/tools/rds/utils'
|
||||||
|
|
||||||
const logger = createLogger('RDSUpdateAPI')
|
const logger = createLogger('RDSUpdateAPI')
|
||||||
@@ -26,11 +25,6 @@ const UpdateSchema = z.object({
|
|||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
const requestId = randomUUID().slice(0, 8)
|
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 {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const params = UpdateSchema.parse(body)
|
const params = UpdateSchema.parse(body)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { createSqsClient, sendMessage } from '../utils'
|
import { createSqsClient, sendMessage } from '../utils'
|
||||||
|
|
||||||
const logger = createLogger('SQSSendMessageAPI')
|
const logger = createLogger('SQSSendMessageAPI')
|
||||||
@@ -22,11 +21,6 @@ const SendMessageSchema = z.object({
|
|||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
const requestId = randomUUID().slice(0, 8)
|
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 {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const params = SendMessageSchema.parse(body)
|
const params = SendMessageSchema.parse(body)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { env } from '@/lib/core/config/env'
|
import { env } from '@/lib/core/config/env'
|
||||||
import { isSensitiveKey, REDACTED_MARKER } from '@/lib/core/security/redaction'
|
import { isSensitiveKey, REDACTED_MARKER } from '@/lib/core/security/redaction'
|
||||||
import { ensureZodObject, normalizeUrl } from '@/app/api/tools/stagehand/utils'
|
import { ensureZodObject, normalizeUrl } from '@/app/api/tools/stagehand/utils'
|
||||||
@@ -92,11 +91,6 @@ function substituteVariables(text: string, variables: Record<string, string> | u
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
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
|
let stagehand: StagehandType | null = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
|
||||||
import { env } from '@/lib/core/config/env'
|
import { env } from '@/lib/core/config/env'
|
||||||
import { ensureZodObject, normalizeUrl } from '@/app/api/tools/stagehand/utils'
|
import { ensureZodObject, normalizeUrl } from '@/app/api/tools/stagehand/utils'
|
||||||
|
|
||||||
@@ -23,11 +22,6 @@ const requestSchema = z.object({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
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
|
let stagehand: StagehandType | null = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -203,9 +203,7 @@ export function ApiInfoModal({ open, onOpenChange, workflowId }: ApiInfoModalPro
|
|||||||
<span className='block truncate font-medium text-[14px] text-[var(--text-tertiary)]'>
|
<span className='block truncate font-medium text-[14px] text-[var(--text-tertiary)]'>
|
||||||
{field.name}
|
{field.name}
|
||||||
</span>
|
</span>
|
||||||
<Badge variant='type' size='sm'>
|
<Badge size='sm'>{field.type || 'string'}</Badge>
|
||||||
{field.type || 'string'}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='border-[var(--border-1)] border-t px-[10px] pt-[6px] pb-[10px]'>
|
<div className='border-[var(--border-1)] border-t px-[10px] pt-[6px] pb-[10px]'>
|
||||||
|
|||||||
@@ -511,9 +511,7 @@ export function McpDeploy({
|
|||||||
<span className='block truncate font-medium text-[14px] text-[var(--text-tertiary)]'>
|
<span className='block truncate font-medium text-[14px] text-[var(--text-tertiary)]'>
|
||||||
{field.name}
|
{field.name}
|
||||||
</span>
|
</span>
|
||||||
<Badge variant='type' size='sm'>
|
<Badge size='sm'>{field.type}</Badge>
|
||||||
{field.type}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='border-[var(--border-1)] border-t px-[10px] pt-[6px] pb-[10px]'>
|
<div className='border-[var(--border-1)] border-t px-[10px] pt-[6px] pb-[10px]'>
|
||||||
|
|||||||
@@ -245,9 +245,7 @@ export function DocumentTagEntry({
|
|||||||
{tag.collapsed ? tag.tagName || `Tag ${index + 1}` : `Tag ${index + 1}`}
|
{tag.collapsed ? tag.tagName || `Tag ${index + 1}` : `Tag ${index + 1}`}
|
||||||
</span>
|
</span>
|
||||||
{tag.collapsed && tag.tagName && (
|
{tag.collapsed && tag.tagName && (
|
||||||
<Badge variant='type' size='sm'>
|
<Badge size='sm'>{FIELD_TYPE_LABELS[tag.fieldType] || 'Text'}</Badge>
|
||||||
{FIELD_TYPE_LABELS[tag.fieldType] || 'Text'}
|
|
||||||
</Badge>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-[8px] pl-[8px]' onClick={(e) => e.stopPropagation()}>
|
<div className='flex items-center gap-[8px] pl-[8px]' onClick={(e) => e.stopPropagation()}>
|
||||||
|
|||||||
@@ -223,11 +223,7 @@ function InputMappingField({
|
|||||||
<span className='block truncate font-medium text-[14px] text-[var(--text-tertiary)]'>
|
<span className='block truncate font-medium text-[14px] text-[var(--text-tertiary)]'>
|
||||||
{fieldName}
|
{fieldName}
|
||||||
</span>
|
</span>
|
||||||
{fieldType && (
|
{fieldType && <Badge size='sm'>{fieldType}</Badge>}
|
||||||
<Badge variant='type' size='sm'>
|
|
||||||
{fieldType}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -238,9 +238,7 @@ export function KnowledgeTagFilters({
|
|||||||
{filter.collapsed ? filter.tagName || `Filter ${index + 1}` : `Filter ${index + 1}`}
|
{filter.collapsed ? filter.tagName || `Filter ${index + 1}` : `Filter ${index + 1}`}
|
||||||
</span>
|
</span>
|
||||||
{filter.collapsed && filter.tagName && (
|
{filter.collapsed && filter.tagName && (
|
||||||
<Badge variant='type' size='sm'>
|
<Badge size='sm'>{FIELD_TYPE_LABELS[filter.fieldType] || 'Text'}</Badge>
|
||||||
{FIELD_TYPE_LABELS[filter.fieldType] || 'Text'}
|
|
||||||
</Badge>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-[8px] pl-[8px]' onClick={(e) => e.stopPropagation()}>
|
<div className='flex items-center gap-[8px] pl-[8px]' onClick={(e) => e.stopPropagation()}>
|
||||||
|
|||||||
@@ -310,11 +310,7 @@ export function FieldFormat({
|
|||||||
<span className='block truncate font-medium text-[14px] text-[var(--text-tertiary)]'>
|
<span className='block truncate font-medium text-[14px] text-[var(--text-tertiary)]'>
|
||||||
{field.name || `${title} ${index + 1}`}
|
{field.name || `${title} ${index + 1}`}
|
||||||
</span>
|
</span>
|
||||||
{field.name && showType && (
|
{field.name && showType && <Badge size='sm'>{field.type}</Badge>}
|
||||||
<Badge variant='type' size='sm'>
|
|
||||||
{field.type}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-[8px] pl-[8px]' onClick={(e) => e.stopPropagation()}>
|
<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'>
|
<Button variant='ghost' onClick={addField} disabled={isReadOnly} className='h-auto p-0'>
|
||||||
|
|||||||
@@ -345,11 +345,7 @@ export function VariablesInput({
|
|||||||
<span className='block truncate font-medium text-[14px] text-[var(--text-tertiary)]'>
|
<span className='block truncate font-medium text-[14px] text-[var(--text-tertiary)]'>
|
||||||
{assignment.variableName || `Variable ${index + 1}`}
|
{assignment.variableName || `Variable ${index + 1}`}
|
||||||
</span>
|
</span>
|
||||||
{assignment.variableName && (
|
{assignment.variableName && <Badge size='sm'>{assignment.type}</Badge>}
|
||||||
<Badge variant='type' size='sm'>
|
|
||||||
{assignment.type}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className='flex items-center gap-[8px] pl-[8px]'
|
className='flex items-center gap-[8px] pl-[8px]'
|
||||||
|
|||||||
@@ -796,7 +796,6 @@ export const Terminal = memo(function Terminal() {
|
|||||||
const terminalRef = useRef<HTMLElement>(null)
|
const terminalRef = useRef<HTMLElement>(null)
|
||||||
const prevEntriesLengthRef = useRef(0)
|
const prevEntriesLengthRef = useRef(0)
|
||||||
const prevWorkflowEntriesLengthRef = useRef(0)
|
const prevWorkflowEntriesLengthRef = useRef(0)
|
||||||
const hasInitializedEntriesRef = useRef(false)
|
|
||||||
const isTerminalFocusedRef = useRef(false)
|
const isTerminalFocusedRef = useRef(false)
|
||||||
const lastExpandedHeightRef = useRef<number>(DEFAULT_EXPANDED_HEIGHT)
|
const lastExpandedHeightRef = useRef<number>(DEFAULT_EXPANDED_HEIGHT)
|
||||||
const setTerminalHeight = useTerminalStore((state) => state.setTerminalHeight)
|
const setTerminalHeight = useTerminalStore((state) => state.setTerminalHeight)
|
||||||
@@ -1008,33 +1007,12 @@ export const Terminal = memo(function Terminal() {
|
|||||||
return JSON.stringify(outputData, null, 2)
|
return JSON.stringify(outputData, null, 2)
|
||||||
}, [outputData])
|
}, [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.
|
* Auto-open the terminal on new entries when "Open on run" is enabled.
|
||||||
* This mirrors the header toggle behavior by using expandToLastHeight,
|
* This mirrors the header toggle behavior by using expandToLastHeight,
|
||||||
* ensuring we always get the same smooth height transition.
|
* 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(() => {
|
useEffect(() => {
|
||||||
if (!hasConsoleHydrated) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasInitializedEntriesRef.current) {
|
|
||||||
hasInitializedEntriesRef.current = true
|
|
||||||
prevWorkflowEntriesLengthRef.current = allWorkflowEntries.length
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!openOnRun) {
|
if (!openOnRun) {
|
||||||
prevWorkflowEntriesLengthRef.current = allWorkflowEntries.length
|
prevWorkflowEntriesLengthRef.current = allWorkflowEntries.length
|
||||||
return
|
return
|
||||||
@@ -1048,14 +1026,7 @@ export const Terminal = memo(function Terminal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
prevWorkflowEntriesLengthRef.current = currentLength
|
prevWorkflowEntriesLengthRef.current = currentLength
|
||||||
}, [
|
}, [allWorkflowEntries.length, expandToLastHeight, openOnRun, isExpanded])
|
||||||
allWorkflowEntries.length,
|
|
||||||
expandToLastHeight,
|
|
||||||
openOnRun,
|
|
||||||
isExpanded,
|
|
||||||
hasConsoleHydrated,
|
|
||||||
activeWorkflowId,
|
|
||||||
])
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle row click - toggle if clicking same entry
|
* Handle row click - toggle if clicking same entry
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ import { useWorkspaceEnvironment } from '@/hooks/queries/environment'
|
|||||||
import { useAutoConnect, useSnapToGridSize } from '@/hooks/queries/general-settings'
|
import { useAutoConnect, useSnapToGridSize } from '@/hooks/queries/general-settings'
|
||||||
import { useCanvasViewport } from '@/hooks/use-canvas-viewport'
|
import { useCanvasViewport } from '@/hooks/use-canvas-viewport'
|
||||||
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
||||||
|
import { usePermissionConfig } from '@/hooks/use-permission-config'
|
||||||
import { useStreamCleanup } from '@/hooks/use-stream-cleanup'
|
import { useStreamCleanup } from '@/hooks/use-stream-cleanup'
|
||||||
import { useCanvasModeStore } from '@/stores/canvas-mode'
|
import { useCanvasModeStore } from '@/stores/canvas-mode'
|
||||||
import { useChatStore } from '@/stores/chat/store'
|
import { useChatStore } from '@/stores/chat/store'
|
||||||
@@ -99,33 +100,39 @@ const logger = createLogger('Workflow')
|
|||||||
const DEFAULT_PASTE_OFFSET = { x: 50, y: 50 }
|
const DEFAULT_PASTE_OFFSET = { x: 50, y: 50 }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates the offset to paste blocks at viewport center, or simple offset for nested blocks
|
* 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
|
||||||
*/
|
*/
|
||||||
function calculatePasteOffset(
|
function calculatePasteOffset(
|
||||||
clipboard: {
|
clipboard: {
|
||||||
blocks: Record<
|
blocks: Record<string, { position: { x: number; y: number }; type: string; height?: number }>
|
||||||
string,
|
|
||||||
{
|
|
||||||
position: { x: number; y: number }
|
|
||||||
type: string
|
|
||||||
height?: number
|
|
||||||
data?: { parentId?: string }
|
|
||||||
}
|
|
||||||
>
|
|
||||||
} | null,
|
} | null,
|
||||||
viewportCenter: { x: number; y: number },
|
screenToFlowPosition: (pos: { x: number; y: number }) => { x: number; y: number }
|
||||||
existingBlocks: Record<string, { id: string }> = {}
|
|
||||||
): { x: number; y: number } {
|
): { x: number; y: number } {
|
||||||
if (!clipboard) return DEFAULT_PASTE_OFFSET
|
if (!clipboard) return DEFAULT_PASTE_OFFSET
|
||||||
|
|
||||||
const clipboardBlocks = Object.values(clipboard.blocks)
|
const clipboardBlocks = Object.values(clipboard.blocks)
|
||||||
if (clipboardBlocks.length === 0) return DEFAULT_PASTE_OFFSET
|
if (clipboardBlocks.length === 0) return DEFAULT_PASTE_OFFSET
|
||||||
|
|
||||||
const allBlocksNested = clipboardBlocks.every(
|
|
||||||
(b) => b.data?.parentId && existingBlocks[b.data.parentId]
|
|
||||||
)
|
|
||||||
if (allBlocksNested) return DEFAULT_PASTE_OFFSET
|
|
||||||
|
|
||||||
const minX = Math.min(...clipboardBlocks.map((b) => b.position.x))
|
const minX = Math.min(...clipboardBlocks.map((b) => b.position.x))
|
||||||
const maxX = Math.max(
|
const maxX = Math.max(
|
||||||
...clipboardBlocks.map((b) => {
|
...clipboardBlocks.map((b) => {
|
||||||
@@ -148,6 +155,8 @@ function calculatePasteOffset(
|
|||||||
)
|
)
|
||||||
const clipboardCenter = { x: (minX + maxX) / 2, y: (minY + maxY) / 2 }
|
const clipboardCenter = { x: (minX + maxX) / 2, y: (minY + maxY) / 2 }
|
||||||
|
|
||||||
|
const viewportCenter = getViewportCenter(screenToFlowPosition)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
x: viewportCenter.x - clipboardCenter.x,
|
x: viewportCenter.x - clipboardCenter.x,
|
||||||
y: viewportCenter.y - clipboardCenter.y,
|
y: viewportCenter.y - clipboardCenter.y,
|
||||||
@@ -257,7 +266,7 @@ const WorkflowContent = React.memo(() => {
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const reactFlowInstance = useReactFlow()
|
const reactFlowInstance = useReactFlow()
|
||||||
const { screenToFlowPosition, getNodes, setNodes, getIntersectingNodes } = reactFlowInstance
|
const { screenToFlowPosition, getNodes, setNodes, getIntersectingNodes } = reactFlowInstance
|
||||||
const { fitViewToBounds, getViewportCenter } = useCanvasViewport(reactFlowInstance)
|
const { fitViewToBounds } = useCanvasViewport(reactFlowInstance)
|
||||||
const { emitCursorUpdate } = useSocket()
|
const { emitCursorUpdate } = useSocket()
|
||||||
|
|
||||||
const workspaceId = params.workspaceId as string
|
const workspaceId = params.workspaceId as string
|
||||||
@@ -321,12 +330,16 @@ const WorkflowContent = React.memo(() => {
|
|||||||
|
|
||||||
const isAutoConnectEnabled = useAutoConnect()
|
const isAutoConnectEnabled = useAutoConnect()
|
||||||
const autoConnectRef = useRef(isAutoConnectEnabled)
|
const autoConnectRef = useRef(isAutoConnectEnabled)
|
||||||
autoConnectRef.current = isAutoConnectEnabled
|
useEffect(() => {
|
||||||
|
autoConnectRef.current = isAutoConnectEnabled
|
||||||
|
}, [isAutoConnectEnabled])
|
||||||
|
|
||||||
// Panel open states for context menu
|
// Panel open states for context menu
|
||||||
const isVariablesOpen = useVariablesStore((state) => state.isOpen)
|
const isVariablesOpen = useVariablesStore((state) => state.isOpen)
|
||||||
const isChatOpen = useChatStore((state) => state.isChatOpen)
|
const isChatOpen = useChatStore((state) => state.isChatOpen)
|
||||||
|
|
||||||
|
// Permission config for invitation control
|
||||||
|
const { isInvitationsDisabled } = usePermissionConfig()
|
||||||
const snapGrid: [number, number] = useMemo(
|
const snapGrid: [number, number] = useMemo(
|
||||||
() => [snapToGridSize, snapToGridSize],
|
() => [snapToGridSize, snapToGridSize],
|
||||||
[snapToGridSize]
|
[snapToGridSize]
|
||||||
@@ -460,16 +473,11 @@ const WorkflowContent = React.memo(() => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
/** Re-applies diff markers when blocks change after socket rehydration. */
|
/** Re-applies diff markers when blocks change after socket rehydration. */
|
||||||
const diffBlocksRef = useRef(blocks)
|
const blocksRef = useRef(blocks)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isWorkflowReady) return
|
if (!isWorkflowReady) return
|
||||||
|
if (hasActiveDiff && isDiffReady && blocks !== blocksRef.current) {
|
||||||
const blocksChanged = blocks !== diffBlocksRef.current
|
blocksRef.current = blocks
|
||||||
if (!blocksChanged) return
|
|
||||||
|
|
||||||
diffBlocksRef.current = blocks
|
|
||||||
|
|
||||||
if (hasActiveDiff && isDiffReady) {
|
|
||||||
setTimeout(() => reapplyDiffMarkers(), 0)
|
setTimeout(() => reapplyDiffMarkers(), 0)
|
||||||
}
|
}
|
||||||
}, [blocks, hasActiveDiff, isDiffReady, reapplyDiffMarkers, isWorkflowReady])
|
}, [blocks, hasActiveDiff, isDiffReady, reapplyDiffMarkers, isWorkflowReady])
|
||||||
@@ -532,7 +540,8 @@ const WorkflowContent = React.memo(() => {
|
|||||||
})
|
})
|
||||||
}, [edges, isShowingDiff, isDiffReady, diffAnalysis, blocks])
|
}, [edges, isShowingDiff, isDiffReady, diffAnalysis, blocks])
|
||||||
|
|
||||||
const { userPermissions } = useWorkspacePermissionsContext()
|
const { userPermissions, workspacePermissions, permissionsError } =
|
||||||
|
useWorkspacePermissionsContext()
|
||||||
|
|
||||||
/** Returns read-only permissions when viewing snapshot, otherwise user permissions. */
|
/** Returns read-only permissions when viewing snapshot, otherwise user permissions. */
|
||||||
const effectivePermissions = useMemo(() => {
|
const effectivePermissions = useMemo(() => {
|
||||||
@@ -768,6 +777,25 @@ const WorkflowContent = React.memo(() => {
|
|||||||
[isErrorConnectionDrag]
|
[isErrorConnectionDrag]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** Logs permission loading results for debugging. */
|
||||||
|
useEffect(() => {
|
||||||
|
if (permissionsError) {
|
||||||
|
logger.error('Failed to load workspace permissions', {
|
||||||
|
workspaceId,
|
||||||
|
error: permissionsError,
|
||||||
|
})
|
||||||
|
} else if (workspacePermissions) {
|
||||||
|
logger.info('Workspace permissions loaded in workflow', {
|
||||||
|
workspaceId,
|
||||||
|
userCount: workspacePermissions.total,
|
||||||
|
permissions: workspacePermissions.users.map((u) => ({
|
||||||
|
email: u.email,
|
||||||
|
permissions: u.permissionType,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [workspacePermissions, permissionsError, workspaceId])
|
||||||
|
|
||||||
const updateNodeParent = useCallback(
|
const updateNodeParent = useCallback(
|
||||||
(nodeId: string, newParentId: string | null, affectedEdges: any[] = []) => {
|
(nodeId: string, newParentId: string | null, affectedEdges: any[] = []) => {
|
||||||
const node = getNodes().find((n: any) => n.id === nodeId)
|
const node = getNodes().find((n: any) => n.id === nodeId)
|
||||||
@@ -873,125 +901,11 @@ const WorkflowContent = React.memo(() => {
|
|||||||
* Consolidates shared logic for context paste, duplicate, and keyboard paste.
|
* Consolidates shared logic for context paste, duplicate, and keyboard paste.
|
||||||
*/
|
*/
|
||||||
const executePasteOperation = useCallback(
|
const executePasteOperation = useCallback(
|
||||||
(
|
(operation: 'paste' | 'duplicate', pasteOffset: { x: number; y: number }) => {
|
||||||
operation: 'paste' | 'duplicate',
|
const pasteData = preparePasteData(pasteOffset)
|
||||||
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
|
if (!pasteData) return
|
||||||
|
|
||||||
let pastedBlocksArray = Object.values(pasteData.blocks)
|
const 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)
|
const validation = validateTriggerPaste(pastedBlocksArray, blocks, operation)
|
||||||
if (!validation.isValid) {
|
if (!validation.isValid) {
|
||||||
addNotification({
|
addNotification({
|
||||||
@@ -1012,47 +926,21 @@ const WorkflowContent = React.memo(() => {
|
|||||||
pasteData.parallels,
|
pasteData.parallels,
|
||||||
pasteData.subBlockValues
|
pasteData.subBlockValues
|
||||||
)
|
)
|
||||||
|
|
||||||
// Resize container if we pasted into a subflow
|
|
||||||
if (targetContainer) {
|
|
||||||
resizeLoopNodesWrapper()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
preparePasteData,
|
preparePasteData,
|
||||||
blocks,
|
blocks,
|
||||||
clipboard,
|
|
||||||
addNotification,
|
addNotification,
|
||||||
activeWorkflowId,
|
activeWorkflowId,
|
||||||
collaborativeBatchAddBlocks,
|
collaborativeBatchAddBlocks,
|
||||||
setPendingSelection,
|
setPendingSelection,
|
||||||
resizeLoopNodesWrapper,
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleContextPaste = useCallback(() => {
|
const handleContextPaste = useCallback(() => {
|
||||||
if (!hasClipboard()) return
|
if (!hasClipboard()) return
|
||||||
|
executePasteOperation('paste', calculatePasteOffset(clipboard, screenToFlowPosition))
|
||||||
// Convert context menu position to flow coordinates and check if inside a subflow
|
}, [hasClipboard, executePasteOperation, clipboard, screenToFlowPosition])
|
||||||
const flowPosition = screenToFlowPosition(contextMenuPosition)
|
|
||||||
const targetContainer = isPointInLoopNode(flowPosition)
|
|
||||||
|
|
||||||
executePasteOperation(
|
|
||||||
'paste',
|
|
||||||
calculatePasteOffset(clipboard, getViewportCenter(), blocks),
|
|
||||||
targetContainer,
|
|
||||||
flowPosition // Pass the click position so blocks are centered at where user right-clicked
|
|
||||||
)
|
|
||||||
}, [
|
|
||||||
hasClipboard,
|
|
||||||
executePasteOperation,
|
|
||||||
clipboard,
|
|
||||||
getViewportCenter,
|
|
||||||
screenToFlowPosition,
|
|
||||||
contextMenuPosition,
|
|
||||||
isPointInLoopNode,
|
|
||||||
blocks,
|
|
||||||
])
|
|
||||||
|
|
||||||
const handleContextDuplicate = useCallback(() => {
|
const handleContextDuplicate = useCallback(() => {
|
||||||
copyBlocks(contextMenuBlocks.map((b) => b.id))
|
copyBlocks(contextMenuBlocks.map((b) => b.id))
|
||||||
@@ -1118,6 +1006,10 @@ const WorkflowContent = React.memo(() => {
|
|||||||
setIsChatOpen(!isChatOpen)
|
setIsChatOpen(!isChatOpen)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const handleContextInvite = useCallback(() => {
|
||||||
|
window.dispatchEvent(new CustomEvent('open-invite-modal'))
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cleanup: (() => void) | null = null
|
let cleanup: (() => void) | null = null
|
||||||
|
|
||||||
@@ -1162,10 +1054,7 @@ const WorkflowContent = React.memo(() => {
|
|||||||
} else if ((event.ctrlKey || event.metaKey) && event.key === 'v') {
|
} else if ((event.ctrlKey || event.metaKey) && event.key === 'v') {
|
||||||
if (effectivePermissions.canEdit && hasClipboard()) {
|
if (effectivePermissions.canEdit && hasClipboard()) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
executePasteOperation(
|
executePasteOperation('paste', calculatePasteOffset(clipboard, screenToFlowPosition))
|
||||||
'paste',
|
|
||||||
calculatePasteOffset(clipboard, getViewportCenter(), blocks)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1185,9 +1074,8 @@ const WorkflowContent = React.memo(() => {
|
|||||||
hasClipboard,
|
hasClipboard,
|
||||||
effectivePermissions.canEdit,
|
effectivePermissions.canEdit,
|
||||||
clipboard,
|
clipboard,
|
||||||
getViewportCenter,
|
screenToFlowPosition,
|
||||||
executePasteOperation,
|
executePasteOperation,
|
||||||
blocks,
|
|
||||||
])
|
])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1619,7 +1507,7 @@ const WorkflowContent = React.memo(() => {
|
|||||||
if (!type) return
|
if (!type) return
|
||||||
if (type === 'connectionBlock') return
|
if (type === 'connectionBlock') return
|
||||||
|
|
||||||
const basePosition = getViewportCenter()
|
const basePosition = getViewportCenter(screenToFlowPosition)
|
||||||
|
|
||||||
if (type === 'loop' || type === 'parallel') {
|
if (type === 'loop' || type === 'parallel') {
|
||||||
const id = crypto.randomUUID()
|
const id = crypto.randomUUID()
|
||||||
@@ -1688,7 +1576,7 @@ const WorkflowContent = React.memo(() => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
getViewportCenter,
|
screenToFlowPosition,
|
||||||
blocks,
|
blocks,
|
||||||
addBlock,
|
addBlock,
|
||||||
effectivePermissions.canEdit,
|
effectivePermissions.canEdit,
|
||||||
@@ -2162,8 +2050,6 @@ const WorkflowContent = React.memo(() => {
|
|||||||
// Local state for nodes - allows smooth drag without store updates on every frame
|
// Local state for nodes - allows smooth drag without store updates on every frame
|
||||||
const [displayNodes, setDisplayNodes] = useState<Node[]>([])
|
const [displayNodes, setDisplayNodes] = useState<Node[]>([])
|
||||||
|
|
||||||
// Sync derivedNodes to displayNodes while preserving selection state
|
|
||||||
// This effect handles both normal sync and pending selection from paste/duplicate
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Check for pending selection (from paste/duplicate), otherwise preserve existing selection
|
// Check for pending selection (from paste/duplicate), otherwise preserve existing selection
|
||||||
if (pendingSelection && pendingSelection.length > 0) {
|
if (pendingSelection && pendingSelection.length > 0) {
|
||||||
@@ -2193,6 +2079,7 @@ const WorkflowContent = React.memo(() => {
|
|||||||
}, [derivedNodes, blocks, pendingSelection, clearPendingSelection])
|
}, [derivedNodes, blocks, pendingSelection, clearPendingSelection])
|
||||||
|
|
||||||
// Phase 2: When displayNodes updates, check if pending zoom blocks are ready
|
// Phase 2: When displayNodes updates, check if pending zoom blocks are ready
|
||||||
|
// (Phase 1 is located earlier in the file where pendingZoomBlockIdsRef is defined)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const pendingBlockIds = pendingZoomBlockIdsRef.current
|
const pendingBlockIds = pendingZoomBlockIdsRef.current
|
||||||
if (!pendingBlockIds || pendingBlockIds.size === 0) {
|
if (!pendingBlockIds || pendingBlockIds.size === 0) {
|
||||||
@@ -2383,6 +2270,40 @@ const WorkflowContent = React.memo(() => {
|
|||||||
resizeLoopNodesWrapper()
|
resizeLoopNodesWrapper()
|
||||||
}, [derivedNodes, resizeLoopNodesWrapper, isWorkflowReady])
|
}, [derivedNodes, resizeLoopNodesWrapper, isWorkflowReady])
|
||||||
|
|
||||||
|
/** Cleans up orphaned nodes with invalid parent references after deletion. */
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isWorkflowReady) return
|
||||||
|
|
||||||
|
// Create a mapping of node IDs to check for missing parent references
|
||||||
|
const nodeIds = new Set(Object.keys(blocks))
|
||||||
|
|
||||||
|
// Check for nodes with invalid parent references and collect updates
|
||||||
|
const orphanedUpdates: Array<{
|
||||||
|
id: string
|
||||||
|
position: { x: number; y: number }
|
||||||
|
parentId: string
|
||||||
|
}> = []
|
||||||
|
Object.entries(blocks).forEach(([id, block]) => {
|
||||||
|
const parentId = block.data?.parentId
|
||||||
|
|
||||||
|
// If block has a parent reference but parent no longer exists
|
||||||
|
if (parentId && !nodeIds.has(parentId)) {
|
||||||
|
logger.warn('Found orphaned node with invalid parent reference', {
|
||||||
|
nodeId: id,
|
||||||
|
missingParentId: parentId,
|
||||||
|
})
|
||||||
|
|
||||||
|
const absolutePosition = getNodeAbsolutePosition(id)
|
||||||
|
orphanedUpdates.push({ id, position: absolutePosition, parentId: '' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Batch update all orphaned nodes at once
|
||||||
|
if (orphanedUpdates.length > 0) {
|
||||||
|
batchUpdateBlocksWithParent(orphanedUpdates)
|
||||||
|
}
|
||||||
|
}, [blocks, batchUpdateBlocksWithParent, getNodeAbsolutePosition, isWorkflowReady])
|
||||||
|
|
||||||
/** Handles edge removal changes. */
|
/** Handles edge removal changes. */
|
||||||
const onEdgesChange = useCallback(
|
const onEdgesChange = useCallback(
|
||||||
(changes: any) => {
|
(changes: any) => {
|
||||||
|
|||||||
@@ -611,9 +611,7 @@ function ServerDetailView({ workspaceId, serverId, onBack }: ServerDetailViewPro
|
|||||||
<span className='block truncate font-medium text-[14px] text-[var(--text-tertiary)]'>
|
<span className='block truncate font-medium text-[14px] text-[var(--text-tertiary)]'>
|
||||||
{name}
|
{name}
|
||||||
</span>
|
</span>
|
||||||
<Badge variant='type' size='sm'>
|
<Badge size='sm'>{prop.type || 'any'}</Badge>
|
||||||
{prop.type || 'any'}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='border-[var(--border-1)] border-t px-[10px] pt-[6px] pb-[10px]'>
|
<div className='border-[var(--border-1)] border-t px-[10px] pt-[6px] pb-[10px]'>
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ 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)]',
|
'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:
|
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)]',
|
'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]`,
|
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)]`,
|
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)]`,
|
gray: `${STATUS_BASE} bg-[#e7e5e4] text-[#57534e] dark:bg-[var(--terminal-status-info-bg)] dark:text-[var(--terminal-status-info-color)]`,
|
||||||
@@ -85,7 +84,7 @@ export interface BadgeProps
|
|||||||
*
|
*
|
||||||
* @remarks
|
* @remarks
|
||||||
* Supports two categories of variants:
|
* Supports two categories of variants:
|
||||||
* - **Bordered**: `default`, `outline`, `type` - traditional badges with borders
|
* - **Bordered**: `default`, `outline` - traditional badges with borders
|
||||||
* - **Status colors**: `green`, `red`, `gray`, `blue`, `blue-secondary`, `purple`,
|
* - **Status colors**: `green`, `red`, `gray`, `blue`, `blue-secondary`, `purple`,
|
||||||
* `orange`, `amber`, `teal`, `cyan`, `gray-secondary` - borderless colored badges
|
* `orange`, `amber`, `teal`, `cyan`, `gray-secondary` - borderless colored badges
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -57,16 +57,31 @@ function getVisibleCanvasBounds(): VisibleBounds {
|
|||||||
* Gets the center of the visible canvas in screen coordinates.
|
* Gets the center of the visible canvas in screen coordinates.
|
||||||
*/
|
*/
|
||||||
function getVisibleCanvasCenter(): { x: number; y: number } {
|
function getVisibleCanvasCenter(): { x: number; y: number } {
|
||||||
const bounds = getVisibleCanvasBounds()
|
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 flowContainer = document.querySelector('.react-flow')
|
const flowContainer = document.querySelector('.react-flow')
|
||||||
const rect = flowContainer?.getBoundingClientRect()
|
if (!flowContainer) {
|
||||||
const containerLeft = rect?.left ?? 0
|
const visibleWidth = window.innerWidth - sidebarWidth - panelWidth
|
||||||
const containerTop = rect?.top ?? 0
|
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)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
x: containerLeft + bounds.offsetLeft + bounds.width / 2,
|
x: (visibleLeft + visibleRight) / 2,
|
||||||
y: containerTop + bounds.height / 2,
|
y: (rect.top + visibleBottom) / 2,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
loadDeployedWorkflowState,
|
loadDeployedWorkflowState,
|
||||||
loadWorkflowFromNormalizedTables,
|
loadWorkflowFromNormalizedTables,
|
||||||
} from '@/lib/workflows/persistence/utils'
|
} from '@/lib/workflows/persistence/utils'
|
||||||
import { mergeSubblockStateWithValues } from '@/lib/workflows/subblocks'
|
|
||||||
import { TriggerUtils } from '@/lib/workflows/triggers/triggers'
|
import { TriggerUtils } from '@/lib/workflows/triggers/triggers'
|
||||||
import { updateWorkflowRunCounts } from '@/lib/workflows/utils'
|
import { updateWorkflowRunCounts } from '@/lib/workflows/utils'
|
||||||
import { Executor } from '@/executor'
|
import { Executor } from '@/executor'
|
||||||
@@ -27,6 +26,7 @@ import type {
|
|||||||
import type { ExecutionResult, NormalizedBlockOutput } from '@/executor/types'
|
import type { ExecutionResult, NormalizedBlockOutput } from '@/executor/types'
|
||||||
import { hasExecutionResult } from '@/executor/utils/errors'
|
import { hasExecutionResult } from '@/executor/utils/errors'
|
||||||
import { Serializer } from '@/serializer'
|
import { Serializer } from '@/serializer'
|
||||||
|
import { mergeSubblockState } from '@/stores/workflows/server-utils'
|
||||||
|
|
||||||
const logger = createLogger('ExecutionCore')
|
const logger = createLogger('ExecutionCore')
|
||||||
|
|
||||||
@@ -172,7 +172,8 @@ export async function executeWorkflowCore(
|
|||||||
logger.info(`[${requestId}] Using deployed workflow state (deployed execution)`)
|
logger.info(`[${requestId}] Using deployed workflow state (deployed execution)`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const mergedStates = mergeSubblockStateWithValues(blocks)
|
// Merge block states
|
||||||
|
const mergedStates = mergeSubblockState(blocks)
|
||||||
|
|
||||||
const personalEnvUserId =
|
const personalEnvUserId =
|
||||||
metadata.isClientSession && metadata.sessionUserId
|
metadata.isClientSession && metadata.sessionUserId
|
||||||
|
|||||||
52
apps/sim/stores/workflows/server-utils.ts
Normal file
52
apps/sim/stores/workflows/server-utils.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* 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'
|
} from '@sim/testing'
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import { normalizeName } from '@/executor/constants'
|
import { normalizeName } from '@/executor/constants'
|
||||||
import { getUniqueBlockName, regenerateBlockIds } from './utils'
|
import { getUniqueBlockName } from './utils'
|
||||||
|
|
||||||
describe('normalizeName', () => {
|
describe('normalizeName', () => {
|
||||||
it.concurrent('should convert to lowercase', () => {
|
it.concurrent('should convert to lowercase', () => {
|
||||||
@@ -223,213 +223,3 @@ describe('getUniqueBlockName', () => {
|
|||||||
expect(getUniqueBlockName('myblock', existingBlocks)).toBe('myblock 2')
|
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,8 +1,7 @@
|
|||||||
import type { Edge } from 'reactflow'
|
import type { Edge } from 'reactflow'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { DEFAULT_DUPLICATE_OFFSET } from '@/lib/workflows/autolayout/constants'
|
|
||||||
import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
|
import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
|
||||||
import { mergeSubblockStateWithValues } from '@/lib/workflows/subblocks'
|
import { mergeSubBlockValues, mergeSubblockStateWithValues } from '@/lib/workflows/subblocks'
|
||||||
import { TriggerUtils } from '@/lib/workflows/triggers/triggers'
|
import { TriggerUtils } from '@/lib/workflows/triggers/triggers'
|
||||||
import { getBlock } from '@/blocks'
|
import { getBlock } from '@/blocks'
|
||||||
import { isAnnotationOnlyBlock, normalizeName } from '@/executor/constants'
|
import { isAnnotationOnlyBlock, normalizeName } from '@/executor/constants'
|
||||||
@@ -17,8 +16,7 @@ import type {
|
|||||||
} from '@/stores/workflows/workflow/types'
|
} from '@/stores/workflows/workflow/types'
|
||||||
import { TRIGGER_RUNTIME_SUBBLOCK_IDS } from '@/triggers/constants'
|
import { TRIGGER_RUNTIME_SUBBLOCK_IDS } from '@/triggers/constants'
|
||||||
|
|
||||||
/** Threshold to detect viewport-based offsets vs small duplicate offsets */
|
const WEBHOOK_SUBBLOCK_FIELDS = ['webhookId', 'triggerPath']
|
||||||
const LARGE_OFFSET_THRESHOLD = 300
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if an edge is valid (source and target exist, not annotation-only, target is not a trigger)
|
* Checks if an edge is valid (source and target exist, not annotation-only, target is not a trigger)
|
||||||
@@ -206,6 +204,64 @@ 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
|
* Merges workflow block states with subblock values while maintaining block structure
|
||||||
* @param blocks - Block configurations from workflow store
|
* @param blocks - Block configurations from workflow store
|
||||||
@@ -292,6 +348,78 @@ 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 {
|
function updateValueReferences(value: unknown, nameMap: Map<string, string>): unknown {
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
let updatedValue = value
|
let updatedValue = value
|
||||||
@@ -316,10 +444,14 @@ function updateValueReferences(value: unknown, nameMap: Map<string, string>): un
|
|||||||
|
|
||||||
function updateBlockReferences(
|
function updateBlockReferences(
|
||||||
blocks: Record<string, BlockState>,
|
blocks: Record<string, BlockState>,
|
||||||
|
idMap: Map<string, string>,
|
||||||
nameMap: Map<string, string>,
|
nameMap: Map<string, string>,
|
||||||
clearTriggerRuntimeValues = false
|
clearTriggerRuntimeValues = false
|
||||||
): void {
|
): void {
|
||||||
Object.entries(blocks).forEach(([_, block]) => {
|
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) {
|
if (block.subBlocks) {
|
||||||
Object.entries(block.subBlocks).forEach(([subBlockId, subBlock]) => {
|
Object.entries(block.subBlocks).forEach(([subBlockId, subBlock]) => {
|
||||||
if (clearTriggerRuntimeValues && TRIGGER_RUNTIME_SUBBLOCK_IDS.includes(subBlockId)) {
|
if (clearTriggerRuntimeValues && TRIGGER_RUNTIME_SUBBLOCK_IDS.includes(subBlockId)) {
|
||||||
@@ -401,7 +533,7 @@ export function regenerateWorkflowIds(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
updateBlockReferences(newBlocks, nameMap, clearTriggerRuntimeValues)
|
updateBlockReferences(newBlocks, blockIdMap, nameMap, clearTriggerRuntimeValues)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
blocks: newBlocks,
|
blocks: newBlocks,
|
||||||
@@ -442,36 +574,13 @@ export function regenerateBlockIds(
|
|||||||
const newNormalizedName = normalizeName(newName)
|
const newNormalizedName = normalizeName(newName)
|
||||||
nameMap.set(oldNormalizedName, newNormalizedName)
|
nameMap.set(oldNormalizedName, newNormalizedName)
|
||||||
|
|
||||||
// Determine position offset based on parent relationship:
|
// Check if this block has a parent that's also being copied
|
||||||
// 1. Parent also being copied: keep exact relative position (parent itself will be offset)
|
// If so, it's a nested block and should keep its relative position (no offset)
|
||||||
// 2. Parent exists in existing workflow: use provided offset, but cap large viewport-based
|
// Only top-level blocks (no parent in the paste set) get the position offset
|
||||||
// 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 hasParentInPasteSet = block.data?.parentId && blocks[block.data.parentId]
|
||||||
const hasParentInExistingWorkflow =
|
const newPosition = hasParentInPasteSet
|
||||||
block.data?.parentId && existingBlockNames[block.data.parentId]
|
? { x: block.position.x, y: block.position.y } // Keep relative position
|
||||||
|
: { x: block.position.x + positionOffset.x, y: block.position.y + positionOffset.y }
|
||||||
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
|
// Placeholder block - we'll update parentId in second pass
|
||||||
const newBlock: BlockState = {
|
const newBlock: BlockState = {
|
||||||
@@ -493,30 +602,19 @@ export function regenerateBlockIds(
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Second pass: update parentId references for nested blocks
|
// Second pass: update parentId references for nested blocks
|
||||||
// If a block's parent is also being pasted, map to new parentId
|
// If a block's parent is also being pasted, map to new parentId; otherwise clear it
|
||||||
// If parent exists in existing workflow, keep the original parentId (block stays in same subflow)
|
|
||||||
// Otherwise clear the parentId
|
|
||||||
Object.entries(newBlocks).forEach(([, block]) => {
|
Object.entries(newBlocks).forEach(([, block]) => {
|
||||||
if (block.data?.parentId) {
|
if (block.data?.parentId) {
|
||||||
const oldParentId = block.data.parentId
|
const oldParentId = block.data.parentId
|
||||||
const newParentId = blockIdMap.get(oldParentId)
|
const newParentId = blockIdMap.get(oldParentId)
|
||||||
|
|
||||||
if (newParentId) {
|
if (newParentId) {
|
||||||
// Parent is being pasted - map to new parent ID
|
|
||||||
block.data = {
|
block.data = {
|
||||||
...block.data,
|
...block.data,
|
||||||
parentId: newParentId,
|
parentId: newParentId,
|
||||||
extent: 'parent',
|
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 {
|
} else {
|
||||||
// Parent doesn't exist anywhere - clear the relationship
|
|
||||||
block.data = { ...block.data, parentId: undefined, extent: undefined }
|
block.data = { ...block.data, parentId: undefined, extent: undefined }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -549,7 +647,7 @@ export function regenerateBlockIds(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
updateBlockReferences(newBlocks, nameMap, false)
|
updateBlockReferences(newBlocks, blockIdMap, nameMap, false)
|
||||||
|
|
||||||
Object.entries(newSubBlockValues).forEach(([_, blockValues]) => {
|
Object.entries(newSubBlockValues).forEach(([_, blockValues]) => {
|
||||||
Object.keys(blockValues).forEach((subBlockId) => {
|
Object.keys(blockValues).forEach((subBlockId) => {
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ import {
|
|||||||
WorkflowBuilder,
|
WorkflowBuilder,
|
||||||
} from '@sim/testing'
|
} from '@sim/testing'
|
||||||
import { beforeEach, describe, expect, it } from 'vitest'
|
import { beforeEach, describe, expect, it } from 'vitest'
|
||||||
|
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||||
|
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||||
|
|
||||||
describe('workflow store', () => {
|
describe('workflow store', () => {
|
||||||
@@ -363,6 +365,30 @@ describe('workflow store', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('duplicateBlock', () => {
|
||||||
|
it('should duplicate a block', () => {
|
||||||
|
const { addBlock, duplicateBlock } = useWorkflowStore.getState()
|
||||||
|
|
||||||
|
addBlock('original', 'agent', 'Original Agent', { x: 0, y: 0 })
|
||||||
|
|
||||||
|
duplicateBlock('original')
|
||||||
|
|
||||||
|
const { blocks } = useWorkflowStore.getState()
|
||||||
|
const blockIds = Object.keys(blocks)
|
||||||
|
|
||||||
|
expect(blockIds.length).toBe(2)
|
||||||
|
|
||||||
|
const duplicatedId = blockIds.find((id) => id !== 'original')
|
||||||
|
expect(duplicatedId).toBeDefined()
|
||||||
|
|
||||||
|
if (duplicatedId) {
|
||||||
|
expect(blocks[duplicatedId].type).toBe('agent')
|
||||||
|
expect(blocks[duplicatedId].name).toContain('Original Agent')
|
||||||
|
expect(blocks[duplicatedId].position.x).not.toBe(0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('batchUpdatePositions', () => {
|
describe('batchUpdatePositions', () => {
|
||||||
it('should update block position', () => {
|
it('should update block position', () => {
|
||||||
const { addBlock, batchUpdatePositions } = useWorkflowStore.getState()
|
const { addBlock, batchUpdatePositions } = useWorkflowStore.getState()
|
||||||
@@ -426,6 +452,29 @@ describe('workflow store', () => {
|
|||||||
expect(state.loops.loop1.forEachItems).toBe('["a", "b", "c"]')
|
expect(state.loops.loop1.forEachItems).toBe('["a", "b", "c"]')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should regenerate loops when updateLoopCollection is called', () => {
|
||||||
|
const { addBlock, updateLoopCollection } = useWorkflowStore.getState()
|
||||||
|
|
||||||
|
addBlock(
|
||||||
|
'loop1',
|
||||||
|
'loop',
|
||||||
|
'Test Loop',
|
||||||
|
{ x: 0, y: 0 },
|
||||||
|
{
|
||||||
|
loopType: 'forEach',
|
||||||
|
collection: '["item1", "item2"]',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
updateLoopCollection('loop1', '["item1", "item2", "item3"]')
|
||||||
|
|
||||||
|
const state = useWorkflowStore.getState()
|
||||||
|
|
||||||
|
expect(state.blocks.loop1?.data?.collection).toBe('["item1", "item2", "item3"]')
|
||||||
|
expect(state.loops.loop1).toBeDefined()
|
||||||
|
expect(state.loops.loop1.forEachItems).toBe('["item1", "item2", "item3"]')
|
||||||
|
})
|
||||||
|
|
||||||
it('should clamp loop count between 1 and 1000', () => {
|
it('should clamp loop count between 1 and 1000', () => {
|
||||||
const { addBlock, updateLoopCount } = useWorkflowStore.getState()
|
const { addBlock, updateLoopCount } = useWorkflowStore.getState()
|
||||||
|
|
||||||
@@ -550,6 +599,118 @@ describe('workflow store', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('mode switching', () => {
|
||||||
|
it('should toggle advanced mode on a block', () => {
|
||||||
|
const { addBlock, toggleBlockAdvancedMode } = useWorkflowStore.getState()
|
||||||
|
|
||||||
|
addBlock('agent1', 'agent', 'Test Agent', { x: 0, y: 0 })
|
||||||
|
|
||||||
|
let state = useWorkflowStore.getState()
|
||||||
|
expect(state.blocks.agent1?.advancedMode).toBe(false)
|
||||||
|
|
||||||
|
toggleBlockAdvancedMode('agent1')
|
||||||
|
state = useWorkflowStore.getState()
|
||||||
|
expect(state.blocks.agent1?.advancedMode).toBe(true)
|
||||||
|
|
||||||
|
toggleBlockAdvancedMode('agent1')
|
||||||
|
state = useWorkflowStore.getState()
|
||||||
|
expect(state.blocks.agent1?.advancedMode).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should preserve systemPrompt and userPrompt when switching modes', () => {
|
||||||
|
const { addBlock, toggleBlockAdvancedMode } = useWorkflowStore.getState()
|
||||||
|
const { setState: setSubBlockState } = useSubBlockStore
|
||||||
|
useWorkflowRegistry.setState({ activeWorkflowId: 'test-workflow' })
|
||||||
|
addBlock('agent1', 'agent', 'Test Agent', { x: 0, y: 0 })
|
||||||
|
setSubBlockState({
|
||||||
|
workflowValues: {
|
||||||
|
'test-workflow': {
|
||||||
|
agent1: {
|
||||||
|
systemPrompt: 'You are a helpful assistant',
|
||||||
|
userPrompt: 'Hello, how are you?',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
toggleBlockAdvancedMode('agent1')
|
||||||
|
let subBlockState = useSubBlockStore.getState()
|
||||||
|
expect(subBlockState.workflowValues['test-workflow'].agent1.systemPrompt).toBe(
|
||||||
|
'You are a helpful assistant'
|
||||||
|
)
|
||||||
|
expect(subBlockState.workflowValues['test-workflow'].agent1.userPrompt).toBe(
|
||||||
|
'Hello, how are you?'
|
||||||
|
)
|
||||||
|
toggleBlockAdvancedMode('agent1')
|
||||||
|
subBlockState = useSubBlockStore.getState()
|
||||||
|
expect(subBlockState.workflowValues['test-workflow'].agent1.systemPrompt).toBe(
|
||||||
|
'You are a helpful assistant'
|
||||||
|
)
|
||||||
|
expect(subBlockState.workflowValues['test-workflow'].agent1.userPrompt).toBe(
|
||||||
|
'Hello, how are you?'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should preserve memories when switching from advanced to basic mode', () => {
|
||||||
|
const { addBlock, toggleBlockAdvancedMode } = useWorkflowStore.getState()
|
||||||
|
const { setState: setSubBlockState } = useSubBlockStore
|
||||||
|
|
||||||
|
useWorkflowRegistry.setState({ activeWorkflowId: 'test-workflow' })
|
||||||
|
|
||||||
|
addBlock('agent1', 'agent', 'Test Agent', { x: 0, y: 0 })
|
||||||
|
|
||||||
|
toggleBlockAdvancedMode('agent1')
|
||||||
|
|
||||||
|
setSubBlockState({
|
||||||
|
workflowValues: {
|
||||||
|
'test-workflow': {
|
||||||
|
agent1: {
|
||||||
|
systemPrompt: 'You are a helpful assistant',
|
||||||
|
userPrompt: 'What did we discuss?',
|
||||||
|
memories: [
|
||||||
|
{ role: 'user', content: 'My name is John' },
|
||||||
|
{ role: 'assistant', content: 'Nice to meet you, John!' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
toggleBlockAdvancedMode('agent1')
|
||||||
|
|
||||||
|
const subBlockState = useSubBlockStore.getState()
|
||||||
|
expect(subBlockState.workflowValues['test-workflow'].agent1.systemPrompt).toBe(
|
||||||
|
'You are a helpful assistant'
|
||||||
|
)
|
||||||
|
expect(subBlockState.workflowValues['test-workflow'].agent1.userPrompt).toBe(
|
||||||
|
'What did we discuss?'
|
||||||
|
)
|
||||||
|
expect(subBlockState.workflowValues['test-workflow'].agent1.memories).toEqual([
|
||||||
|
{ role: 'user', content: 'My name is John' },
|
||||||
|
{ role: 'assistant', content: 'Nice to meet you, John!' },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle mode switching when no subblock values exist', () => {
|
||||||
|
const { addBlock, toggleBlockAdvancedMode } = useWorkflowStore.getState()
|
||||||
|
|
||||||
|
useWorkflowRegistry.setState({ activeWorkflowId: 'test-workflow' })
|
||||||
|
|
||||||
|
addBlock('agent1', 'agent', 'Test Agent', { x: 0, y: 0 })
|
||||||
|
|
||||||
|
expect(useWorkflowStore.getState().blocks.agent1?.advancedMode).toBe(false)
|
||||||
|
expect(() => toggleBlockAdvancedMode('agent1')).not.toThrow()
|
||||||
|
|
||||||
|
const state = useWorkflowStore.getState()
|
||||||
|
expect(state.blocks.agent1?.advancedMode).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not throw when toggling non-existent block', () => {
|
||||||
|
const { toggleBlockAdvancedMode } = useWorkflowStore.getState()
|
||||||
|
|
||||||
|
expect(() => toggleBlockAdvancedMode('non-existent')).not.toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('workflow state management', () => {
|
describe('workflow state management', () => {
|
||||||
it('should work with WorkflowBuilder for complex setups', () => {
|
it('should work with WorkflowBuilder for complex setups', () => {
|
||||||
const workflowState = WorkflowBuilder.linear(3).build()
|
const workflowState = WorkflowBuilder.linear(3).build()
|
||||||
|
|||||||
@@ -2,16 +2,20 @@ import { createLogger } from '@sim/logger'
|
|||||||
import type { Edge } from 'reactflow'
|
import type { Edge } from 'reactflow'
|
||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import { devtools } from 'zustand/middleware'
|
import { devtools } from 'zustand/middleware'
|
||||||
import { CONTAINER_DIMENSIONS } from '@/lib/workflows/blocks/block-dimensions'
|
import { DEFAULT_DUPLICATE_OFFSET } from '@/lib/workflows/autolayout/constants'
|
||||||
import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
|
import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
|
||||||
import { getBlock } from '@/blocks'
|
import { getBlock } from '@/blocks'
|
||||||
import type { SubBlockConfig } from '@/blocks/types'
|
import type { SubBlockConfig } from '@/blocks/types'
|
||||||
import { normalizeName, RESERVED_BLOCK_NAMES } from '@/executor/constants'
|
import { normalizeName, RESERVED_BLOCK_NAMES } from '@/executor/constants'
|
||||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||||
import { filterNewEdges, filterValidEdges } from '@/stores/workflows/utils'
|
import {
|
||||||
|
filterNewEdges,
|
||||||
|
filterValidEdges,
|
||||||
|
getUniqueBlockName,
|
||||||
|
mergeSubblockState,
|
||||||
|
} from '@/stores/workflows/utils'
|
||||||
import type {
|
import type {
|
||||||
BlockState,
|
|
||||||
Position,
|
Position,
|
||||||
SubBlockState,
|
SubBlockState,
|
||||||
WorkflowState,
|
WorkflowState,
|
||||||
@@ -135,30 +139,30 @@ export const useWorkflowStore = create<WorkflowStore>()(
|
|||||||
...(parentId && { parentId, extent: extent || 'parent' }),
|
...(parentId && { parentId, extent: extent || 'parent' }),
|
||||||
}
|
}
|
||||||
|
|
||||||
const newBlocks = {
|
const newState = {
|
||||||
...get().blocks,
|
blocks: {
|
||||||
[id]: {
|
...get().blocks,
|
||||||
id,
|
[id]: {
|
||||||
type,
|
id,
|
||||||
name,
|
type,
|
||||||
position,
|
name,
|
||||||
subBlocks: {},
|
position,
|
||||||
outputs: {},
|
subBlocks: {},
|
||||||
enabled: blockProperties?.enabled ?? true,
|
outputs: {},
|
||||||
horizontalHandles: blockProperties?.horizontalHandles ?? true,
|
enabled: blockProperties?.enabled ?? true,
|
||||||
advancedMode: blockProperties?.advancedMode ?? false,
|
horizontalHandles: blockProperties?.horizontalHandles ?? true,
|
||||||
triggerMode: blockProperties?.triggerMode ?? false,
|
advancedMode: blockProperties?.advancedMode ?? false,
|
||||||
height: blockProperties?.height ?? 0,
|
triggerMode: blockProperties?.triggerMode ?? false,
|
||||||
data: nodeData,
|
height: blockProperties?.height ?? 0,
|
||||||
|
data: nodeData,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
edges: [...get().edges],
|
||||||
|
loops: get().generateLoopBlocks(),
|
||||||
|
parallels: get().generateParallelBlocks(),
|
||||||
}
|
}
|
||||||
|
|
||||||
set({
|
set(newState)
|
||||||
blocks: newBlocks,
|
|
||||||
edges: [...get().edges],
|
|
||||||
loops: generateLoopBlocks(newBlocks),
|
|
||||||
parallels: generateParallelBlocks(newBlocks),
|
|
||||||
})
|
|
||||||
get().updateLastSaved()
|
get().updateLastSaved()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -211,31 +215,31 @@ export const useWorkflowStore = create<WorkflowStore>()(
|
|||||||
const triggerMode = blockProperties?.triggerMode ?? false
|
const triggerMode = blockProperties?.triggerMode ?? false
|
||||||
const outputs = getBlockOutputs(type, subBlocks, triggerMode)
|
const outputs = getBlockOutputs(type, subBlocks, triggerMode)
|
||||||
|
|
||||||
const newBlocks = {
|
const newState = {
|
||||||
...get().blocks,
|
blocks: {
|
||||||
[id]: {
|
...get().blocks,
|
||||||
id,
|
[id]: {
|
||||||
type,
|
id,
|
||||||
name,
|
type,
|
||||||
position,
|
name,
|
||||||
subBlocks,
|
position,
|
||||||
outputs,
|
subBlocks,
|
||||||
enabled: blockProperties?.enabled ?? true,
|
outputs,
|
||||||
horizontalHandles: blockProperties?.horizontalHandles ?? true,
|
enabled: blockProperties?.enabled ?? true,
|
||||||
advancedMode: blockProperties?.advancedMode ?? false,
|
horizontalHandles: blockProperties?.horizontalHandles ?? true,
|
||||||
triggerMode: triggerMode,
|
advancedMode: blockProperties?.advancedMode ?? false,
|
||||||
height: blockProperties?.height ?? 0,
|
triggerMode: triggerMode,
|
||||||
layout: {},
|
height: blockProperties?.height ?? 0,
|
||||||
data: nodeData,
|
layout: {},
|
||||||
|
data: nodeData,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
edges: [...get().edges],
|
||||||
|
loops: get().generateLoopBlocks(),
|
||||||
|
parallels: get().generateParallelBlocks(),
|
||||||
}
|
}
|
||||||
|
|
||||||
set({
|
set(newState)
|
||||||
blocks: newBlocks,
|
|
||||||
edges: [...get().edges],
|
|
||||||
loops: generateLoopBlocks(newBlocks),
|
|
||||||
parallels: generateParallelBlocks(newBlocks),
|
|
||||||
})
|
|
||||||
get().updateLastSaved()
|
get().updateLastSaved()
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -444,41 +448,6 @@ export const useWorkflowStore = create<WorkflowStore>()(
|
|||||||
delete newBlocks[blockId]
|
delete newBlocks[blockId]
|
||||||
})
|
})
|
||||||
|
|
||||||
// Clean up orphaned nodes - blocks whose parent was removed but weren't descendants
|
|
||||||
// This can happen in edge cases (e.g., data inconsistency, external modifications)
|
|
||||||
const remainingBlockIds = new Set(Object.keys(newBlocks))
|
|
||||||
const CONTAINER_OFFSET = {
|
|
||||||
x: CONTAINER_DIMENSIONS.LEFT_PADDING,
|
|
||||||
y: CONTAINER_DIMENSIONS.HEADER_HEIGHT + CONTAINER_DIMENSIONS.TOP_PADDING,
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.entries(newBlocks).forEach(([blockId, block]) => {
|
|
||||||
const parentId = block.data?.parentId
|
|
||||||
if (parentId && !remainingBlockIds.has(parentId)) {
|
|
||||||
// Parent was removed - convert to absolute position and clear parentId
|
|
||||||
// Child positions are relative to container content area (after header + padding)
|
|
||||||
let absoluteX = block.position.x
|
|
||||||
let absoluteY = block.position.y
|
|
||||||
|
|
||||||
// Traverse up the parent chain, adding position + container offset for each level
|
|
||||||
let currentParentId: string | undefined = parentId
|
|
||||||
while (currentParentId) {
|
|
||||||
const parent: BlockState | undefined = currentBlocks[currentParentId]
|
|
||||||
if (!parent) break
|
|
||||||
absoluteX += parent.position.x + CONTAINER_OFFSET.x
|
|
||||||
absoluteY += parent.position.y + CONTAINER_OFFSET.y
|
|
||||||
currentParentId = parent.data?.parentId
|
|
||||||
}
|
|
||||||
|
|
||||||
const { parentId: _removed, extent: _removedExtent, ...restData } = block.data || {}
|
|
||||||
newBlocks[blockId] = {
|
|
||||||
...block,
|
|
||||||
position: { x: absoluteX, y: absoluteY },
|
|
||||||
data: Object.keys(restData).length > 0 ? restData : undefined,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
||||||
if (activeWorkflowId) {
|
if (activeWorkflowId) {
|
||||||
const subBlockStore = useSubBlockStore.getState()
|
const subBlockStore = useSubBlockStore.getState()
|
||||||
@@ -615,20 +584,7 @@ export const useWorkflowStore = create<WorkflowStore>()(
|
|||||||
options?: { updateLastSaved?: boolean }
|
options?: { updateLastSaved?: boolean }
|
||||||
) => {
|
) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const incomingBlocks = workflowState.blocks || {}
|
const nextBlocks = workflowState.blocks || {}
|
||||||
|
|
||||||
const nextBlocks: typeof incomingBlocks = {}
|
|
||||||
for (const [id, block] of Object.entries(incomingBlocks)) {
|
|
||||||
if (block.data?.parentId && !incomingBlocks[block.data.parentId]) {
|
|
||||||
nextBlocks[id] = {
|
|
||||||
...block,
|
|
||||||
data: { ...block.data, parentId: undefined, extent: undefined },
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
nextBlocks[id] = block
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextEdges = filterValidEdges(workflowState.edges || [], nextBlocks)
|
const nextEdges = filterValidEdges(workflowState.edges || [], nextBlocks)
|
||||||
const nextLoops =
|
const nextLoops =
|
||||||
Object.keys(workflowState.loops || {}).length > 0
|
Object.keys(workflowState.loops || {}).length > 0
|
||||||
@@ -679,6 +635,66 @@ export const useWorkflowStore = create<WorkflowStore>()(
|
|||||||
get().updateLastSaved()
|
get().updateLastSaved()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
duplicateBlock: (id: string) => {
|
||||||
|
const block = get().blocks[id]
|
||||||
|
if (!block) return
|
||||||
|
|
||||||
|
const newId = crypto.randomUUID()
|
||||||
|
const offsetPosition = {
|
||||||
|
x: block.position.x + DEFAULT_DUPLICATE_OFFSET.x,
|
||||||
|
y: block.position.y + DEFAULT_DUPLICATE_OFFSET.y,
|
||||||
|
}
|
||||||
|
|
||||||
|
const newName = getUniqueBlockName(block.name, get().blocks)
|
||||||
|
|
||||||
|
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
||||||
|
const mergedBlock = mergeSubblockState(get().blocks, activeWorkflowId || undefined, id)[id]
|
||||||
|
|
||||||
|
const newSubBlocks = Object.entries(mergedBlock.subBlocks).reduce(
|
||||||
|
(acc, [subId, subBlock]) => ({
|
||||||
|
...acc,
|
||||||
|
[subId]: {
|
||||||
|
...subBlock,
|
||||||
|
value: JSON.parse(JSON.stringify(subBlock.value)),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
|
const newState = {
|
||||||
|
blocks: {
|
||||||
|
...get().blocks,
|
||||||
|
[newId]: {
|
||||||
|
...block,
|
||||||
|
id: newId,
|
||||||
|
name: newName,
|
||||||
|
position: offsetPosition,
|
||||||
|
subBlocks: newSubBlocks,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
edges: [...get().edges],
|
||||||
|
loops: get().generateLoopBlocks(),
|
||||||
|
parallels: get().generateParallelBlocks(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeWorkflowId) {
|
||||||
|
const subBlockValues =
|
||||||
|
useSubBlockStore.getState().workflowValues[activeWorkflowId]?.[id] || {}
|
||||||
|
useSubBlockStore.setState((state) => ({
|
||||||
|
workflowValues: {
|
||||||
|
...state.workflowValues,
|
||||||
|
[activeWorkflowId]: {
|
||||||
|
...state.workflowValues[activeWorkflowId],
|
||||||
|
[newId]: JSON.parse(JSON.stringify(subBlockValues)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
set(newState)
|
||||||
|
get().updateLastSaved()
|
||||||
|
},
|
||||||
|
|
||||||
setBlockHandles: (id: string, horizontalHandles: boolean) => {
|
setBlockHandles: (id: string, horizontalHandles: boolean) => {
|
||||||
const block = get().blocks[id]
|
const block = get().blocks[id]
|
||||||
if (!block || block.horizontalHandles === horizontalHandles) return
|
if (!block || block.horizontalHandles === horizontalHandles) return
|
||||||
@@ -874,10 +890,27 @@ export const useWorkflowStore = create<WorkflowStore>()(
|
|||||||
get().updateLastSaved()
|
get().updateLastSaved()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setBlockTriggerMode: (id: string, triggerMode: boolean) => {
|
||||||
|
set((state) => ({
|
||||||
|
blocks: {
|
||||||
|
...state.blocks,
|
||||||
|
[id]: {
|
||||||
|
...state.blocks[id],
|
||||||
|
triggerMode,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
edges: [...state.edges],
|
||||||
|
loops: { ...state.loops },
|
||||||
|
}))
|
||||||
|
get().updateLastSaved()
|
||||||
|
// Note: Socket.IO handles real-time sync automatically
|
||||||
|
},
|
||||||
|
|
||||||
updateBlockLayoutMetrics: (id: string, dimensions: { width: number; height: number }) => {
|
updateBlockLayoutMetrics: (id: string, dimensions: { width: number; height: number }) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const block = state.blocks[id]
|
const block = state.blocks[id]
|
||||||
if (!block) {
|
if (!block) {
|
||||||
|
logger.warn(`Cannot update layout metrics: Block ${id} not found in workflow store`)
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -899,6 +932,7 @@ export const useWorkflowStore = create<WorkflowStore>()(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
get().updateLastSaved()
|
get().updateLastSaved()
|
||||||
|
// No sync needed for layout changes, just visual
|
||||||
},
|
},
|
||||||
|
|
||||||
updateLoopCount: (loopId: string, count: number) =>
|
updateLoopCount: (loopId: string, count: number) =>
|
||||||
@@ -1016,6 +1050,30 @@ export const useWorkflowStore = create<WorkflowStore>()(
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
updateLoopCollection: (loopId: string, collection: string) => {
|
||||||
|
const store = get()
|
||||||
|
const block = store.blocks[loopId]
|
||||||
|
if (!block || block.type !== 'loop') return
|
||||||
|
|
||||||
|
const loopType = block.data?.loopType || 'for'
|
||||||
|
|
||||||
|
if (loopType === 'while') {
|
||||||
|
store.setLoopWhileCondition(loopId, collection)
|
||||||
|
} else if (loopType === 'doWhile') {
|
||||||
|
store.setLoopDoWhileCondition(loopId, collection)
|
||||||
|
} else if (loopType === 'forEach') {
|
||||||
|
store.setLoopForEachItems(loopId, collection)
|
||||||
|
} else {
|
||||||
|
// Default to forEach-style storage for backward compatibility
|
||||||
|
store.setLoopForEachItems(loopId, collection)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Function to convert UI loop blocks to execution format
|
||||||
|
generateLoopBlocks: () => {
|
||||||
|
return generateLoopBlocks(get().blocks)
|
||||||
|
},
|
||||||
|
|
||||||
triggerUpdate: () => {
|
triggerUpdate: () => {
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
...state,
|
...state,
|
||||||
@@ -1103,6 +1161,28 @@ export const useWorkflowStore = create<WorkflowStore>()(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toggleBlockAdvancedMode: (id: string) => {
|
||||||
|
const block = get().blocks[id]
|
||||||
|
if (!block) return
|
||||||
|
|
||||||
|
const newState = {
|
||||||
|
blocks: {
|
||||||
|
...get().blocks,
|
||||||
|
[id]: {
|
||||||
|
...block,
|
||||||
|
advancedMode: !block.advancedMode,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
edges: [...get().edges],
|
||||||
|
loops: { ...get().loops },
|
||||||
|
}
|
||||||
|
|
||||||
|
set(newState)
|
||||||
|
|
||||||
|
get().triggerUpdate()
|
||||||
|
// Note: Socket.IO handles real-time sync automatically
|
||||||
|
},
|
||||||
|
|
||||||
// Parallel block methods implementation
|
// Parallel block methods implementation
|
||||||
updateParallelCount: (parallelId: string, count: number) => {
|
updateParallelCount: (parallelId: string, count: number) => {
|
||||||
const block = get().blocks[parallelId]
|
const block = get().blocks[parallelId]
|
||||||
@@ -1128,6 +1208,7 @@ export const useWorkflowStore = create<WorkflowStore>()(
|
|||||||
|
|
||||||
set(newState)
|
set(newState)
|
||||||
get().updateLastSaved()
|
get().updateLastSaved()
|
||||||
|
// Note: Socket.IO handles real-time sync automatically
|
||||||
},
|
},
|
||||||
|
|
||||||
updateParallelCollection: (parallelId: string, collection: string) => {
|
updateParallelCollection: (parallelId: string, collection: string) => {
|
||||||
@@ -1154,6 +1235,7 @@ export const useWorkflowStore = create<WorkflowStore>()(
|
|||||||
|
|
||||||
set(newState)
|
set(newState)
|
||||||
get().updateLastSaved()
|
get().updateLastSaved()
|
||||||
|
// Note: Socket.IO handles real-time sync automatically
|
||||||
},
|
},
|
||||||
|
|
||||||
updateParallelType: (parallelId: string, parallelType: 'count' | 'collection') => {
|
updateParallelType: (parallelId: string, parallelType: 'count' | 'collection') => {
|
||||||
@@ -1180,6 +1262,12 @@ export const useWorkflowStore = create<WorkflowStore>()(
|
|||||||
|
|
||||||
set(newState)
|
set(newState)
|
||||||
get().updateLastSaved()
|
get().updateLastSaved()
|
||||||
|
// Note: Socket.IO handles real-time sync automatically
|
||||||
|
},
|
||||||
|
|
||||||
|
// Function to convert UI parallel blocks to execution format
|
||||||
|
generateParallelBlocks: () => {
|
||||||
|
return generateParallelBlocks(get().blocks)
|
||||||
},
|
},
|
||||||
|
|
||||||
setDragStartPosition: (position) => {
|
setDragStartPosition: (position) => {
|
||||||
|
|||||||
@@ -214,6 +214,7 @@ export interface WorkflowActions {
|
|||||||
clear: () => Partial<WorkflowState>
|
clear: () => Partial<WorkflowState>
|
||||||
updateLastSaved: () => void
|
updateLastSaved: () => void
|
||||||
setBlockEnabled: (id: string, enabled: boolean) => void
|
setBlockEnabled: (id: string, enabled: boolean) => void
|
||||||
|
duplicateBlock: (id: string) => void
|
||||||
setBlockHandles: (id: string, horizontalHandles: boolean) => void
|
setBlockHandles: (id: string, horizontalHandles: boolean) => void
|
||||||
updateBlockName: (
|
updateBlockName: (
|
||||||
id: string,
|
id: string,
|
||||||
@@ -224,18 +225,23 @@ export interface WorkflowActions {
|
|||||||
}
|
}
|
||||||
setBlockAdvancedMode: (id: string, advancedMode: boolean) => void
|
setBlockAdvancedMode: (id: string, advancedMode: boolean) => void
|
||||||
setBlockCanonicalMode: (id: string, canonicalId: string, mode: 'basic' | 'advanced') => void
|
setBlockCanonicalMode: (id: string, canonicalId: string, mode: 'basic' | 'advanced') => void
|
||||||
|
setBlockTriggerMode: (id: string, triggerMode: boolean) => void
|
||||||
updateBlockLayoutMetrics: (id: string, dimensions: { width: number; height: number }) => void
|
updateBlockLayoutMetrics: (id: string, dimensions: { width: number; height: number }) => void
|
||||||
triggerUpdate: () => void
|
triggerUpdate: () => void
|
||||||
updateLoopCount: (loopId: string, count: number) => void
|
updateLoopCount: (loopId: string, count: number) => void
|
||||||
updateLoopType: (loopId: string, loopType: 'for' | 'forEach' | 'while' | 'doWhile') => void
|
updateLoopType: (loopId: string, loopType: 'for' | 'forEach' | 'while' | 'doWhile') => void
|
||||||
|
updateLoopCollection: (loopId: string, collection: string) => void
|
||||||
setLoopForEachItems: (loopId: string, items: any) => void
|
setLoopForEachItems: (loopId: string, items: any) => void
|
||||||
setLoopWhileCondition: (loopId: string, condition: string) => void
|
setLoopWhileCondition: (loopId: string, condition: string) => void
|
||||||
setLoopDoWhileCondition: (loopId: string, condition: string) => void
|
setLoopDoWhileCondition: (loopId: string, condition: string) => void
|
||||||
updateParallelCount: (parallelId: string, count: number) => void
|
updateParallelCount: (parallelId: string, count: number) => void
|
||||||
updateParallelCollection: (parallelId: string, collection: string) => void
|
updateParallelCollection: (parallelId: string, collection: string) => void
|
||||||
updateParallelType: (parallelId: string, parallelType: 'count' | 'collection') => void
|
updateParallelType: (parallelId: string, parallelType: 'count' | 'collection') => void
|
||||||
|
generateLoopBlocks: () => Record<string, Loop>
|
||||||
|
generateParallelBlocks: () => Record<string, Parallel>
|
||||||
setNeedsRedeploymentFlag: (needsRedeployment: boolean) => void
|
setNeedsRedeploymentFlag: (needsRedeployment: boolean) => void
|
||||||
revertToDeployedState: (deployedState: WorkflowState) => void
|
revertToDeployedState: (deployedState: WorkflowState) => void
|
||||||
|
toggleBlockAdvancedMode: (id: string) => void
|
||||||
setDragStartPosition: (position: DragStartPosition | null) => void
|
setDragStartPosition: (position: DragStartPosition | null) => void
|
||||||
getDragStartPosition: () => DragStartPosition | null
|
getDragStartPosition: () => DragStartPosition | null
|
||||||
getWorkflowState: () => WorkflowState
|
getWorkflowState: () => WorkflowState
|
||||||
|
|||||||
Reference in New Issue
Block a user