Skip to content

Commit 9009b54

Browse files
committed
feat(clickup): KB connector (Docs v3), selector-route hardening, subblock migrations, registry-check regex fix
1 parent e8c4ff7 commit 9009b54

13 files changed

Lines changed: 657 additions & 203 deletions

File tree

apps/sim/app/api/tools/clickup/folders/route.ts

Lines changed: 62 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
33
import { clickupFoldersSelectorContract } from '@/lib/api/contracts/selectors'
44
import { parseRequest } from '@/lib/api/server'
55
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
6+
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
67
import { generateRequestId } from '@/lib/core/utils/request'
78
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
89
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
@@ -18,64 +19,75 @@ interface ClickUpNamedResource {
1819
}
1920

2021
export const POST = withRouteHandler(async (request: NextRequest) => {
21-
const requestId = generateRequestId()
22-
const parsed = await parseRequest(clickupFoldersSelectorContract, request, {})
23-
if (!parsed.success) return parsed.response
24-
const { credential, workflowId, spaceId } = parsed.data.body
22+
try {
23+
const requestId = generateRequestId()
24+
const parsed = await parseRequest(clickupFoldersSelectorContract, request, {})
25+
if (!parsed.success) return parsed.response
26+
const { credential, workflowId, spaceId } = parsed.data.body
2527

26-
const authz = await authorizeCredentialUse(request, {
27-
credentialId: credential,
28-
workflowId,
29-
})
30-
if (!authz.ok || !authz.credentialOwnerUserId) {
31-
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
32-
}
28+
const spaceIdValidation = validateAlphanumericId(spaceId, 'spaceId')
29+
if (!spaceIdValidation.isValid) {
30+
logger.error('Invalid spaceId', { error: spaceIdValidation.error })
31+
return NextResponse.json({ error: spaceIdValidation.error }, { status: 400 })
32+
}
3333

34-
const accessToken = await refreshAccessTokenIfNeeded(
35-
credential,
36-
authz.credentialOwnerUserId,
37-
requestId
38-
)
39-
if (!accessToken) {
40-
logger.error('Failed to get access token', {
34+
const authz = await authorizeCredentialUse(request, {
4135
credentialId: credential,
42-
userId: authz.credentialOwnerUserId,
36+
workflowId,
4337
})
44-
return NextResponse.json(
45-
{ error: 'Could not retrieve access token', authRequired: true },
46-
{ status: 401 }
47-
)
48-
}
38+
if (!authz.ok || !authz.credentialOwnerUserId) {
39+
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
40+
}
4941

50-
const response = await fetch(
51-
`${CLICKUP_API_BASE_URL}/space/${encodeURIComponent(spaceId)}/folder`,
52-
{
53-
headers: {
54-
Authorization: clickupAuthorizationHeader(accessToken),
55-
Accept: 'application/json',
56-
},
42+
const accessToken = await refreshAccessTokenIfNeeded(
43+
credential,
44+
authz.credentialOwnerUserId,
45+
requestId
46+
)
47+
if (!accessToken) {
48+
logger.error('Failed to get access token', {
49+
credentialId: credential,
50+
userId: authz.credentialOwnerUserId,
51+
})
52+
return NextResponse.json(
53+
{ error: 'Could not retrieve access token', authRequired: true },
54+
{ status: 401 }
55+
)
5756
}
58-
)
5957

60-
if (!response.ok) {
61-
const errorData = await response.json().catch(() => ({}))
62-
logger.error('Failed to fetch ClickUp folders', {
63-
status: response.status,
64-
error: errorData,
65-
})
66-
return NextResponse.json(
67-
{ error: 'Failed to fetch ClickUp folders' },
68-
{ status: response.status }
58+
const response = await fetch(
59+
`${CLICKUP_API_BASE_URL}/space/${encodeURIComponent(spaceId)}/folder`,
60+
{
61+
headers: {
62+
Authorization: clickupAuthorizationHeader(accessToken),
63+
Accept: 'application/json',
64+
},
65+
}
6966
)
70-
}
7167

72-
const data = (await response.json().catch(() => ({}))) as {
73-
folders?: ClickUpNamedResource[]
74-
}
75-
const folders = (Array.isArray(data.folders) ? data.folders : []).map((item) => ({
76-
id: String(item.id),
77-
name: item.name || `Folder ${item.id}`,
78-
}))
68+
if (!response.ok) {
69+
const errorData = await response.json().catch(() => ({}))
70+
logger.error('Failed to fetch ClickUp folders', {
71+
status: response.status,
72+
error: errorData,
73+
})
74+
return NextResponse.json(
75+
{ error: 'Failed to fetch ClickUp folders' },
76+
{ status: response.status }
77+
)
78+
}
7979

80-
return NextResponse.json({ folders })
80+
const data = (await response.json().catch(() => ({}))) as {
81+
folders?: ClickUpNamedResource[]
82+
}
83+
const folders = (Array.isArray(data.folders) ? data.folders : []).map((item) => ({
84+
id: String(item.id),
85+
name: item.name || `Folder ${item.id}`,
86+
}))
87+
88+
return NextResponse.json({ folders })
89+
} catch (error) {
90+
logger.error('Error fetching ClickUp folders', error)
91+
return NextResponse.json({ error: 'Failed to fetch ClickUp folders' }, { status: 500 })
92+
}
8193
})

apps/sim/app/api/tools/clickup/lists/route.ts

Lines changed: 74 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
33
import { clickupListsSelectorContract } from '@/lib/api/contracts/selectors'
44
import { parseRequest } from '@/lib/api/server'
55
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
6+
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
67
import { generateRequestId } from '@/lib/core/utils/request'
78
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
89
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
@@ -18,66 +19,87 @@ interface ClickUpNamedResource {
1819
}
1920

2021
export const POST = withRouteHandler(async (request: NextRequest) => {
21-
const requestId = generateRequestId()
22-
const parsed = await parseRequest(clickupListsSelectorContract, request, {})
23-
if (!parsed.success) return parsed.response
24-
const { credential, workflowId, folderId, spaceId } = parsed.data.body
22+
try {
23+
const requestId = generateRequestId()
24+
const parsed = await parseRequest(clickupListsSelectorContract, request, {})
25+
if (!parsed.success) return parsed.response
26+
const { credential, workflowId, folderId, spaceId } = parsed.data.body
2527

26-
const authz = await authorizeCredentialUse(request, {
27-
credentialId: credential,
28-
workflowId,
29-
})
30-
if (!authz.ok || !authz.credentialOwnerUserId) {
31-
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
32-
}
28+
if (folderId?.trim()) {
29+
const folderIdValidation = validateAlphanumericId(folderId.trim(), 'folderId')
30+
if (!folderIdValidation.isValid) {
31+
logger.error('Invalid folderId', { error: folderIdValidation.error })
32+
return NextResponse.json({ error: folderIdValidation.error }, { status: 400 })
33+
}
34+
}
35+
36+
if (spaceId?.trim()) {
37+
const spaceIdValidation = validateAlphanumericId(spaceId.trim(), 'spaceId')
38+
if (!spaceIdValidation.isValid) {
39+
logger.error('Invalid spaceId', { error: spaceIdValidation.error })
40+
return NextResponse.json({ error: spaceIdValidation.error }, { status: 400 })
41+
}
42+
}
3343

34-
const accessToken = await refreshAccessTokenIfNeeded(
35-
credential,
36-
authz.credentialOwnerUserId,
37-
requestId
38-
)
39-
if (!accessToken) {
40-
logger.error('Failed to get access token', {
44+
const authz = await authorizeCredentialUse(request, {
4145
credentialId: credential,
42-
userId: authz.credentialOwnerUserId,
46+
workflowId,
4347
})
44-
return NextResponse.json(
45-
{ error: 'Could not retrieve access token', authRequired: true },
46-
{ status: 401 }
47-
)
48-
}
48+
if (!authz.ok || !authz.credentialOwnerUserId) {
49+
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
50+
}
4951

50-
const response = await fetch(
51-
folderId?.trim()
52-
? `${CLICKUP_API_BASE_URL}/folder/${encodeURIComponent(folderId.trim())}/list`
53-
: `${CLICKUP_API_BASE_URL}/space/${encodeURIComponent((spaceId ?? '').trim())}/list`,
54-
{
55-
headers: {
56-
Authorization: clickupAuthorizationHeader(accessToken),
57-
Accept: 'application/json',
58-
},
52+
const accessToken = await refreshAccessTokenIfNeeded(
53+
credential,
54+
authz.credentialOwnerUserId,
55+
requestId
56+
)
57+
if (!accessToken) {
58+
logger.error('Failed to get access token', {
59+
credentialId: credential,
60+
userId: authz.credentialOwnerUserId,
61+
})
62+
return NextResponse.json(
63+
{ error: 'Could not retrieve access token', authRequired: true },
64+
{ status: 401 }
65+
)
5966
}
60-
)
6167

62-
if (!response.ok) {
63-
const errorData = await response.json().catch(() => ({}))
64-
logger.error('Failed to fetch ClickUp lists', {
65-
status: response.status,
66-
error: errorData,
67-
})
68-
return NextResponse.json(
69-
{ error: 'Failed to fetch ClickUp lists' },
70-
{ status: response.status }
68+
const response = await fetch(
69+
folderId?.trim()
70+
? `${CLICKUP_API_BASE_URL}/folder/${encodeURIComponent(folderId.trim())}/list`
71+
: `${CLICKUP_API_BASE_URL}/space/${encodeURIComponent((spaceId ?? '').trim())}/list`,
72+
{
73+
headers: {
74+
Authorization: clickupAuthorizationHeader(accessToken),
75+
Accept: 'application/json',
76+
},
77+
}
7178
)
72-
}
7379

74-
const data = (await response.json().catch(() => ({}))) as {
75-
lists?: ClickUpNamedResource[]
76-
}
77-
const lists = (Array.isArray(data.lists) ? data.lists : []).map((item) => ({
78-
id: String(item.id),
79-
name: item.name || `List ${item.id}`,
80-
}))
80+
if (!response.ok) {
81+
const errorData = await response.json().catch(() => ({}))
82+
logger.error('Failed to fetch ClickUp lists', {
83+
status: response.status,
84+
error: errorData,
85+
})
86+
return NextResponse.json(
87+
{ error: 'Failed to fetch ClickUp lists' },
88+
{ status: response.status }
89+
)
90+
}
8191

82-
return NextResponse.json({ lists })
92+
const data = (await response.json().catch(() => ({}))) as {
93+
lists?: ClickUpNamedResource[]
94+
}
95+
const lists = (Array.isArray(data.lists) ? data.lists : []).map((item) => ({
96+
id: String(item.id),
97+
name: item.name || `List ${item.id}`,
98+
}))
99+
100+
return NextResponse.json({ lists })
101+
} catch (error) {
102+
logger.error('Error fetching ClickUp lists', error)
103+
return NextResponse.json({ error: 'Failed to fetch ClickUp lists' }, { status: 500 })
104+
}
83105
})

0 commit comments

Comments
 (0)