Skip to content

Commit 0992a6a

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(jsm): resolve selector contexts server-side
1 parent 0b8f18c commit 0992a6a

6 files changed

Lines changed: 366 additions & 99 deletions

File tree

apps/sim/app/api/tools/jsm/selector-requesttypes/route.ts

Lines changed: 57 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,20 @@ import { createLogger } from '@sim/logger'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { jsmRequestTypesSelectorContract } from '@/lib/api/contracts/selectors/jsm'
44
import { parseRequest } from '@/lib/api/server'
5-
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
65
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
76
import { generateRequestId } from '@/lib/core/utils/request'
87
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
9-
import { refreshAccessTokenIfNeeded } from '@/lib/oauth/credential-service'
10-
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
8+
import { resolveAtlassianSelectorCredential } from '@/lib/selectors/application/atlassian-credential'
9+
import {
10+
resolveSelectorProviderValue,
11+
SELECTOR_ATLASSIAN_DISCOVERY_OPTIONS,
12+
selectorProviderFailure,
13+
} from '@/lib/selectors/server/provider-errors'
14+
import {
15+
authenticateSelectorRequest,
16+
resolveAuthorizedSelectorContext,
17+
} from '@/lib/selectors/server/resolve-authorized-context'
18+
import { getJiraCloudId } from '@/tools/jira/utils'
1119
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
1220

1321
const logger = createLogger('JsmSelectorRequestTypesAPI')
@@ -83,20 +91,20 @@ async function fetchAllJsmRequestTypes(
8391
export const POST = withRouteHandler(async (request: NextRequest) => {
8492
const requestId = generateRequestId()
8593
try {
94+
const authentication = await authenticateSelectorRequest(request)
95+
if (!authentication.ok) {
96+
return NextResponse.json({ error: authentication.error }, { status: authentication.status })
97+
}
8698
const parsed = await parseRequest(jsmRequestTypesSelectorContract, request, {})
8799
if (!parsed.success) return parsed.response
88100

89-
const { credential, workflowId, domain, serviceDeskId } = parsed.data.body
101+
const { credential, workflowId, domain: domainReference, serviceDeskId } = parsed.data.body
90102

91103
if (!credential) {
92104
logger.error('Missing credential in request')
93105
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
94106
}
95107

96-
if (!domain) {
97-
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
98-
}
99-
100108
if (!serviceDeskId) {
101109
return NextResponse.json({ error: 'Service Desk ID is required' }, { status: 400 })
102110
}
@@ -106,31 +114,50 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
106114
return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 })
107115
}
108116

109-
const authz = await authorizeCredentialUse(request, {
110-
credentialId: credential,
117+
const resolution = await resolveAuthorizedSelectorContext(authentication.principal, {
111118
workflowId,
119+
credentialId: credential,
120+
context: { domain: domainReference },
112121
})
113-
if (!authz.ok || !authz.credentialOwnerUserId) {
114-
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
122+
if (!resolution.ok) {
123+
return NextResponse.json({ error: resolution.error }, { status: resolution.status })
115124
}
116-
117-
const accessToken = await refreshAccessTokenIfNeeded(
118-
credential,
119-
authz.credentialOwnerUserId,
120-
requestId
121-
)
122-
if (!accessToken) {
123-
logger.error('Failed to get access token', {
124-
credentialId: credential,
125-
userId: authz.credentialOwnerUserId,
126-
})
125+
const ownerUserId = resolution.credentialAccess?.credentialOwnerUserId
126+
if (!ownerUserId) {
127+
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
128+
}
129+
const bundle = await resolveAtlassianSelectorCredential({
130+
credentialId: credential,
131+
credentialOwnerUserId: ownerUserId,
132+
requestId,
133+
serviceId: 'jira',
134+
})
135+
if (!bundle) {
136+
logger.error('Failed to get JSM selector access token')
127137
return NextResponse.json(
128138
{ error: 'Could not retrieve access token', authRequired: true },
129139
{ status: 401 }
130140
)
131141
}
132142

133-
const cloudId = await getJiraCloudId(domain, accessToken)
143+
const domain = resolution.context.domain as string
144+
const accessToken = bundle.accessToken
145+
const cloudIdResolution = await resolveSelectorProviderValue(
146+
'Jira Service Management',
147+
async () =>
148+
bundle.cloudId
149+
? bundle.cloudId
150+
: getJiraCloudId(domain, accessToken, SELECTOR_ATLASSIAN_DISCOVERY_OPTIONS)
151+
)
152+
if (!cloudIdResolution.ok) {
153+
logger.warn('JSM selector discovery failed', {
154+
status: cloudIdResolution.upstreamStatus ?? 'unknown',
155+
})
156+
return NextResponse.json(cloudIdResolution.failure, {
157+
status: cloudIdResolution.failure.status,
158+
})
159+
}
160+
const cloudId = cloudIdResolution.value
134161

135162
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
136163
if (!cloudIdValidation.isValid) {
@@ -143,22 +170,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
143170
const { values, lastResponse } = await fetchAllJsmRequestTypes(requestTypeUrl, accessToken)
144171

145172
if (!lastResponse.ok) {
146-
const errorText = await lastResponse.text()
147-
logger.error('JSM API error:', {
148-
status: lastResponse.status,
149-
statusText: lastResponse.statusText,
150-
error: errorText,
151-
})
152-
return NextResponse.json(
153-
{
154-
error: parseAtlassianErrorMessage(
155-
lastResponse.status,
156-
lastResponse.statusText,
157-
errorText
158-
),
159-
},
160-
{ status: lastResponse.status }
161-
)
173+
logger.warn('JSM selector request-type request failed', { status: lastResponse.status })
174+
const failure = selectorProviderFailure('Jira Service Management', lastResponse.status)
175+
return NextResponse.json(failure, { status: failure.status })
162176
}
163177

164178
const requestTypes = values.map((rt) => ({
@@ -167,11 +181,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
167181
}))
168182

169183
return NextResponse.json({ requestTypes })
170-
} catch (error) {
171-
logger.error('Error listing JSM request types:', error)
172-
return NextResponse.json(
173-
{ error: (error as Error).message || 'Internal server error' },
174-
{ status: 500 }
175-
)
184+
} catch {
185+
logger.error('Error listing JSM request types')
186+
return NextResponse.json({ error: 'Failed to retrieve JSM request types' }, { status: 500 })
176187
}
177188
})

apps/sim/app/api/tools/jsm/selector-servicedesks/route.ts

Lines changed: 57 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,20 @@ import { createLogger } from '@sim/logger'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { jsmServiceDesksSelectorContract } from '@/lib/api/contracts/selectors/jsm'
44
import { parseRequest } from '@/lib/api/server'
5-
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
65
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
76
import { generateRequestId } from '@/lib/core/utils/request'
87
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
9-
import { refreshAccessTokenIfNeeded } from '@/lib/oauth/credential-service'
10-
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
8+
import { resolveAtlassianSelectorCredential } from '@/lib/selectors/application/atlassian-credential'
9+
import {
10+
resolveSelectorProviderValue,
11+
SELECTOR_ATLASSIAN_DISCOVERY_OPTIONS,
12+
selectorProviderFailure,
13+
} from '@/lib/selectors/server/provider-errors'
14+
import {
15+
authenticateSelectorRequest,
16+
resolveAuthorizedSelectorContext,
17+
} from '@/lib/selectors/server/resolve-authorized-context'
18+
import { getJiraCloudId } from '@/tools/jira/utils'
1119
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
1220

1321
const logger = createLogger('JsmSelectorServiceDesksAPI')
@@ -83,45 +91,64 @@ async function fetchAllJsmServiceDesks(
8391
export const POST = withRouteHandler(async (request: NextRequest) => {
8492
const requestId = generateRequestId()
8593
try {
94+
const authentication = await authenticateSelectorRequest(request)
95+
if (!authentication.ok) {
96+
return NextResponse.json({ error: authentication.error }, { status: authentication.status })
97+
}
8698
const parsed = await parseRequest(jsmServiceDesksSelectorContract, request, {})
8799
if (!parsed.success) return parsed.response
88100

89-
const { credential, workflowId, domain } = parsed.data.body
101+
const { credential, workflowId, domain: domainReference } = parsed.data.body
90102

91103
if (!credential) {
92104
logger.error('Missing credential in request')
93105
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
94106
}
95107

96-
if (!domain) {
97-
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
98-
}
99-
100-
const authz = await authorizeCredentialUse(request, {
101-
credentialId: credential,
108+
const resolution = await resolveAuthorizedSelectorContext(authentication.principal, {
102109
workflowId,
110+
credentialId: credential,
111+
context: { domain: domainReference },
103112
})
104-
if (!authz.ok || !authz.credentialOwnerUserId) {
105-
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
113+
if (!resolution.ok) {
114+
return NextResponse.json({ error: resolution.error }, { status: resolution.status })
106115
}
107-
108-
const accessToken = await refreshAccessTokenIfNeeded(
109-
credential,
110-
authz.credentialOwnerUserId,
111-
requestId
112-
)
113-
if (!accessToken) {
114-
logger.error('Failed to get access token', {
115-
credentialId: credential,
116-
userId: authz.credentialOwnerUserId,
117-
})
116+
const ownerUserId = resolution.credentialAccess?.credentialOwnerUserId
117+
if (!ownerUserId) {
118+
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
119+
}
120+
const bundle = await resolveAtlassianSelectorCredential({
121+
credentialId: credential,
122+
credentialOwnerUserId: ownerUserId,
123+
requestId,
124+
serviceId: 'jira',
125+
})
126+
if (!bundle) {
127+
logger.error('Failed to get JSM selector access token')
118128
return NextResponse.json(
119129
{ error: 'Could not retrieve access token', authRequired: true },
120130
{ status: 401 }
121131
)
122132
}
123133

124-
const cloudId = await getJiraCloudId(domain, accessToken)
134+
const domain = resolution.context.domain as string
135+
const accessToken = bundle.accessToken
136+
const cloudIdResolution = await resolveSelectorProviderValue(
137+
'Jira Service Management',
138+
async () =>
139+
bundle.cloudId
140+
? bundle.cloudId
141+
: getJiraCloudId(domain, accessToken, SELECTOR_ATLASSIAN_DISCOVERY_OPTIONS)
142+
)
143+
if (!cloudIdResolution.ok) {
144+
logger.warn('JSM selector discovery failed', {
145+
status: cloudIdResolution.upstreamStatus ?? 'unknown',
146+
})
147+
return NextResponse.json(cloudIdResolution.failure, {
148+
status: cloudIdResolution.failure.status,
149+
})
150+
}
151+
const cloudId = cloudIdResolution.value
125152

126153
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
127154
if (!cloudIdValidation.isValid) {
@@ -133,22 +160,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
133160
const { values, lastResponse } = await fetchAllJsmServiceDesks(baseUrl, accessToken)
134161

135162
if (!lastResponse.ok) {
136-
const errorText = await lastResponse.text()
137-
logger.error('JSM API error:', {
138-
status: lastResponse.status,
139-
statusText: lastResponse.statusText,
140-
error: errorText,
141-
})
142-
return NextResponse.json(
143-
{
144-
error: parseAtlassianErrorMessage(
145-
lastResponse.status,
146-
lastResponse.statusText,
147-
errorText
148-
),
149-
},
150-
{ status: lastResponse.status }
151-
)
163+
logger.warn('JSM selector service-desk request failed', { status: lastResponse.status })
164+
const failure = selectorProviderFailure('Jira Service Management', lastResponse.status)
165+
return NextResponse.json(failure, { status: failure.status })
152166
}
153167

154168
const serviceDesks = values.map((sd) => ({
@@ -157,11 +171,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
157171
}))
158172

159173
return NextResponse.json({ serviceDesks })
160-
} catch (error) {
161-
logger.error('Error listing JSM service desks:', error)
162-
return NextResponse.json(
163-
{ error: (error as Error).message || 'Internal server error' },
164-
{ status: 500 }
165-
)
174+
} catch {
175+
logger.error('Error listing JSM service desks')
176+
return NextResponse.json({ error: 'Failed to retrieve JSM service desks' }, { status: 500 })
166177
}
167178
})

0 commit comments

Comments
 (0)