chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmApprovalsContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import {
|
||||
validateAlphanumericId,
|
||||
validateEnum,
|
||||
validateJiraCloudId,
|
||||
validateJiraIssueKey,
|
||||
} from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmApprovalsAPI')
|
||||
|
||||
const VALID_ACTIONS = ['get', 'answer'] as const
|
||||
const VALID_DECISIONS = ['approve', 'decline'] as const
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmApprovalsContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: cloudIdParam,
|
||||
action,
|
||||
issueIdOrKey,
|
||||
approvalId,
|
||||
decision,
|
||||
start,
|
||||
limit,
|
||||
} = parsed.data.body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!issueIdOrKey) {
|
||||
logger.error('Missing issueIdOrKey in request')
|
||||
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!action) {
|
||||
logger.error('Missing action in request')
|
||||
return NextResponse.json({ error: 'Action is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const actionValidation = validateEnum(action, VALID_ACTIONS, 'action')
|
||||
if (!actionValidation.isValid) {
|
||||
return NextResponse.json({ error: actionValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
|
||||
if (!issueIdOrKeyValidation.isValid) {
|
||||
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
if (action === 'get') {
|
||||
const params = new URLSearchParams()
|
||||
if (start) params.append('start', start)
|
||||
if (limit) params.append('limit', limit)
|
||||
|
||||
const url = `${baseUrl}/request/${issueIdOrKey}/approval${params.toString() ? `?${params.toString()}` : ''}`
|
||||
|
||||
logger.info('Fetching approvals from:', url)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueIdOrKey,
|
||||
approvals: data.values || [],
|
||||
total: data.size || 0,
|
||||
isLastPage: data.isLastPage ?? true,
|
||||
},
|
||||
})
|
||||
}
|
||||
if (action === 'answer') {
|
||||
if (!approvalId) {
|
||||
logger.error('Missing approvalId in request')
|
||||
return NextResponse.json({ error: 'Approval ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const approvalIdValidation = validateAlphanumericId(approvalId, 'approvalId')
|
||||
if (!approvalIdValidation.isValid) {
|
||||
return NextResponse.json({ error: approvalIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const decisionValidation = validateEnum(decision, VALID_DECISIONS, 'decision')
|
||||
if (!decisionValidation.isValid) {
|
||||
return NextResponse.json({ error: decisionValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const url = `${baseUrl}/request/${issueIdOrKey}/approval/${approvalId}`
|
||||
|
||||
logger.info('Answering approval:', { issueIdOrKey, approvalId, decision })
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
body: JSON.stringify({ decision }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueIdOrKey,
|
||||
approvalId,
|
||||
decision,
|
||||
id: data.id ?? null,
|
||||
name: data.name ?? null,
|
||||
finalDecision: data.finalDecision ?? null,
|
||||
canAnswerApproval: data.canAnswerApproval ?? null,
|
||||
approvers: (data.approvers ?? []).map((a: Record<string, unknown>) => {
|
||||
const approver = a.approver as Record<string, unknown> | undefined
|
||||
return {
|
||||
approver: {
|
||||
accountId: approver?.accountId ?? null,
|
||||
displayName: approver?.displayName ?? null,
|
||||
emailAddress: approver?.emailAddress ?? null,
|
||||
active: approver?.active ?? null,
|
||||
},
|
||||
approverDecision: a.approverDecision ?? null,
|
||||
}
|
||||
}),
|
||||
createdDate: data.createdDate ?? null,
|
||||
completedDate: data.completedDate ?? null,
|
||||
approval: data,
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
|
||||
} catch (error) {
|
||||
logger.error('Error in approvals operation:', {
|
||||
error: toError(error).message,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: getErrorMessage(error, 'Internal server error'),
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,98 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmObjectTypeAttributesContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import {
|
||||
validateAssetsWorkspaceId,
|
||||
validateJiraCloudId,
|
||||
} from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getAssetsApiBaseUrl, getJsmHeaders, resolveAssetsContext } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmAssetsAttributesAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmObjectTypeAttributesContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: cloudIdParam,
|
||||
workspaceId: workspaceIdParam,
|
||||
objectTypeId,
|
||||
onlyValueEditable,
|
||||
query: searchQuery,
|
||||
} = parsed.data.body
|
||||
|
||||
const { cloudId, workspaceId } = await resolveAssetsContext(
|
||||
domain,
|
||||
accessToken,
|
||||
cloudIdParam,
|
||||
workspaceIdParam
|
||||
)
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
|
||||
if (!workspaceIdValidation.isValid) {
|
||||
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const query = new URLSearchParams()
|
||||
if (onlyValueEditable !== undefined) {
|
||||
query.append('onlyValueEditable', String(onlyValueEditable))
|
||||
}
|
||||
if (searchQuery) query.append('query', searchQuery)
|
||||
|
||||
const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/objecttype/${encodeURIComponent(
|
||||
objectTypeId
|
||||
)}/attributes${query.toString() ? `?${query.toString()}` : ''}`
|
||||
|
||||
const response = await fetch(url, { method: 'GET', headers: getJsmHeaders(accessToken) })
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('Assets API error getting attributes', { status: response.status, errorText })
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const attributes = Array.isArray(data) ? data : (data.values ?? [])
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
attributes,
|
||||
total: attributes.length,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error getting Assets attributes', { error: toError(error).message })
|
||||
return NextResponse.json(
|
||||
{ error: getErrorMessage(error, 'Internal server error'), success: false },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,97 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmListObjectTypesContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import {
|
||||
validateAssetsWorkspaceId,
|
||||
validateJiraCloudId,
|
||||
} from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getAssetsApiBaseUrl, getJsmHeaders, resolveAssetsContext } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmAssetsObjectTypesAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmListObjectTypesContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: cloudIdParam,
|
||||
workspaceId: workspaceIdParam,
|
||||
schemaId,
|
||||
excludeAbstract,
|
||||
} = parsed.data.body
|
||||
|
||||
const { cloudId, workspaceId } = await resolveAssetsContext(
|
||||
domain,
|
||||
accessToken,
|
||||
cloudIdParam,
|
||||
workspaceIdParam
|
||||
)
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
|
||||
if (!workspaceIdValidation.isValid) {
|
||||
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const query = new URLSearchParams()
|
||||
if (excludeAbstract !== undefined) query.append('excludeAbstract', String(excludeAbstract))
|
||||
|
||||
const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/objectschema/${encodeURIComponent(
|
||||
schemaId
|
||||
)}/objecttypes${query.toString() ? `?${query.toString()}` : ''}`
|
||||
|
||||
const response = await fetch(url, { method: 'GET', headers: getJsmHeaders(accessToken) })
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('Assets API error listing object types', {
|
||||
status: response.status,
|
||||
errorText,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const objectTypes = Array.isArray(data) ? data : (data.values ?? [])
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
objectTypes,
|
||||
total: objectTypes.length,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error listing Assets object types', { error: toError(error).message })
|
||||
return NextResponse.json(
|
||||
{ error: getErrorMessage(error, 'Internal server error'), success: false },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,93 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmCreateObjectContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import {
|
||||
validateAssetsWorkspaceId,
|
||||
validateJiraCloudId,
|
||||
} from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import {
|
||||
getAssetsApiBaseUrl,
|
||||
getJsmHeaders,
|
||||
mapAssetObject,
|
||||
resolveAssetsContext,
|
||||
} from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmAssetsCreateObjectAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmCreateObjectContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: cloudIdParam,
|
||||
workspaceId: workspaceIdParam,
|
||||
objectTypeId,
|
||||
attributes,
|
||||
} = parsed.data.body
|
||||
|
||||
const { cloudId, workspaceId } = await resolveAssetsContext(
|
||||
domain,
|
||||
accessToken,
|
||||
cloudIdParam,
|
||||
workspaceIdParam
|
||||
)
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
|
||||
if (!workspaceIdValidation.isValid) {
|
||||
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/object/create`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
body: JSON.stringify({ objectTypeId, attributes }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('Assets API error creating object', { status: response.status, errorText })
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { ts: new Date().toISOString(), object: mapAssetObject(data) },
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error creating Assets object', { error: toError(error).message })
|
||||
return NextResponse.json(
|
||||
{ error: getErrorMessage(error, 'Internal server error'), success: false },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,81 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmDeleteObjectContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import {
|
||||
validateAssetsWorkspaceId,
|
||||
validateJiraCloudId,
|
||||
} from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getAssetsApiBaseUrl, getJsmHeaders, resolveAssetsContext } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmAssetsDeleteObjectAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmDeleteObjectContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: cloudIdParam,
|
||||
workspaceId: workspaceIdParam,
|
||||
objectId,
|
||||
} = parsed.data.body
|
||||
|
||||
const { cloudId, workspaceId } = await resolveAssetsContext(
|
||||
domain,
|
||||
accessToken,
|
||||
cloudIdParam,
|
||||
workspaceIdParam
|
||||
)
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
|
||||
if (!workspaceIdValidation.isValid) {
|
||||
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/object/${encodeURIComponent(objectId)}`
|
||||
|
||||
const response = await fetch(url, { method: 'DELETE', headers: getJsmHeaders(accessToken) })
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('Assets API error deleting object', { status: response.status, errorText })
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { ts: new Date().toISOString(), objectId, deleted: true },
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error deleting Assets object', { error: toError(error).message })
|
||||
return NextResponse.json(
|
||||
{ error: getErrorMessage(error, 'Internal server error'), success: false },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,88 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmGetObjectContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import {
|
||||
validateAssetsWorkspaceId,
|
||||
validateJiraCloudId,
|
||||
} from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import {
|
||||
getAssetsApiBaseUrl,
|
||||
getJsmHeaders,
|
||||
mapAssetObject,
|
||||
resolveAssetsContext,
|
||||
} from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmAssetsGetObjectAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmGetObjectContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: cloudIdParam,
|
||||
workspaceId: workspaceIdParam,
|
||||
objectId,
|
||||
} = parsed.data.body
|
||||
|
||||
const { cloudId, workspaceId } = await resolveAssetsContext(
|
||||
domain,
|
||||
accessToken,
|
||||
cloudIdParam,
|
||||
workspaceIdParam
|
||||
)
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
|
||||
if (!workspaceIdValidation.isValid) {
|
||||
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/object/${encodeURIComponent(objectId)}`
|
||||
|
||||
const response = await fetch(url, { method: 'GET', headers: getJsmHeaders(accessToken) })
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('Assets API error getting object', { status: response.status, errorText })
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { ts: new Date().toISOString(), object: mapAssetObject(data) },
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error getting Assets object', { error: toError(error).message })
|
||||
return NextResponse.json(
|
||||
{ error: getErrorMessage(error, 'Internal server error'), success: false },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,97 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmUpdateObjectContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import {
|
||||
validateAssetsWorkspaceId,
|
||||
validateJiraCloudId,
|
||||
} from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import {
|
||||
getAssetsApiBaseUrl,
|
||||
getJsmHeaders,
|
||||
mapAssetObject,
|
||||
resolveAssetsContext,
|
||||
} from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmAssetsUpdateObjectAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmUpdateObjectContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: cloudIdParam,
|
||||
workspaceId: workspaceIdParam,
|
||||
objectId,
|
||||
objectTypeId,
|
||||
attributes,
|
||||
} = parsed.data.body
|
||||
|
||||
const { cloudId, workspaceId } = await resolveAssetsContext(
|
||||
domain,
|
||||
accessToken,
|
||||
cloudIdParam,
|
||||
workspaceIdParam
|
||||
)
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
|
||||
if (!workspaceIdValidation.isValid) {
|
||||
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/object/${encodeURIComponent(objectId)}`
|
||||
|
||||
const body: Record<string, unknown> = { attributes }
|
||||
if (objectTypeId) body.objectTypeId = objectTypeId
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('Assets API error updating object', { status: response.status, errorText })
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { ts: new Date().toISOString(), object: mapAssetObject(data) },
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error updating Assets object', { error: toError(error).message })
|
||||
return NextResponse.json(
|
||||
{ error: getErrorMessage(error, 'Internal server error'), success: false },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,83 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmGetObjectSchemaContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import {
|
||||
validateAssetsWorkspaceId,
|
||||
validateJiraCloudId,
|
||||
} from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getAssetsApiBaseUrl, getJsmHeaders, resolveAssetsContext } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmAssetsSchemaAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmGetObjectSchemaContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: cloudIdParam,
|
||||
workspaceId: workspaceIdParam,
|
||||
schemaId,
|
||||
} = parsed.data.body
|
||||
|
||||
const { cloudId, workspaceId } = await resolveAssetsContext(
|
||||
domain,
|
||||
accessToken,
|
||||
cloudIdParam,
|
||||
workspaceIdParam
|
||||
)
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
|
||||
if (!workspaceIdValidation.isValid) {
|
||||
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/objectschema/${encodeURIComponent(schemaId)}`
|
||||
|
||||
const response = await fetch(url, { method: 'GET', headers: getJsmHeaders(accessToken) })
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('Assets API error getting schema', { status: response.status, errorText })
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { ts: new Date().toISOString(), schema: data ?? null },
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error getting Assets schema', { error: toError(error).message })
|
||||
return NextResponse.json(
|
||||
{ error: getErrorMessage(error, 'Internal server error'), success: false },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,97 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmListObjectSchemasContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import {
|
||||
validateAssetsWorkspaceId,
|
||||
validateJiraCloudId,
|
||||
} from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getAssetsApiBaseUrl, getJsmHeaders, resolveAssetsContext } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmAssetsSchemasAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmListObjectSchemasContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: cloudIdParam,
|
||||
workspaceId: workspaceIdParam,
|
||||
startAt,
|
||||
maxResults,
|
||||
includeCounts,
|
||||
} = parsed.data.body
|
||||
|
||||
const { cloudId, workspaceId } = await resolveAssetsContext(
|
||||
domain,
|
||||
accessToken,
|
||||
cloudIdParam,
|
||||
workspaceIdParam
|
||||
)
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
|
||||
if (!workspaceIdValidation.isValid) {
|
||||
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const query = new URLSearchParams()
|
||||
if (startAt !== undefined) query.append('startAt', String(startAt))
|
||||
if (maxResults !== undefined) query.append('maxResults', String(maxResults))
|
||||
if (includeCounts !== undefined) query.append('includeCounts', String(includeCounts))
|
||||
|
||||
const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/objectschema/list${
|
||||
query.toString() ? `?${query.toString()}` : ''
|
||||
}`
|
||||
|
||||
const response = await fetch(url, { method: 'GET', headers: getJsmHeaders(accessToken) })
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('Assets API error listing schemas', { status: response.status, errorText })
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
schemas: data.values ?? [],
|
||||
total: data.total ?? (data.values?.length || 0),
|
||||
isLast: data.isLast ?? data.last ?? true,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error listing Assets schemas', { error: toError(error).message })
|
||||
return NextResponse.json(
|
||||
{ error: getErrorMessage(error, 'Internal server error'), success: false },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,122 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmSearchObjectsAqlContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import {
|
||||
validateAssetsWorkspaceId,
|
||||
validateJiraCloudId,
|
||||
} from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import {
|
||||
getAssetsApiBaseUrl,
|
||||
getJsmHeaders,
|
||||
mapAssetObject,
|
||||
resolveAssetsContext,
|
||||
} from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmAssetsSearchAPI')
|
||||
|
||||
/** Coerce a string|number|boolean param into a number, falling back when unset */
|
||||
function toNumber(value: string | number | undefined, fallback: number): number {
|
||||
if (value === undefined) return fallback
|
||||
const parsed = typeof value === 'number' ? value : Number(value)
|
||||
return Number.isFinite(parsed) ? parsed : fallback
|
||||
}
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmSearchObjectsAqlContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: cloudIdParam,
|
||||
workspaceId: workspaceIdParam,
|
||||
qlQuery,
|
||||
page,
|
||||
resultsPerPage,
|
||||
includeAttributes,
|
||||
objectTypeId,
|
||||
objectSchemaId,
|
||||
} = parsed.data.body
|
||||
|
||||
const { cloudId, workspaceId } = await resolveAssetsContext(
|
||||
domain,
|
||||
accessToken,
|
||||
cloudIdParam,
|
||||
workspaceIdParam
|
||||
)
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
|
||||
if (!workspaceIdValidation.isValid) {
|
||||
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const includeAttrs =
|
||||
includeAttributes === undefined ? true : String(includeAttributes) === 'true'
|
||||
|
||||
const body: Record<string, unknown> = {
|
||||
qlQuery,
|
||||
page: toNumber(page, 1),
|
||||
resultsPerPage: toNumber(resultsPerPage, 25),
|
||||
includeAttributes: includeAttrs,
|
||||
}
|
||||
if (objectTypeId) body.objectTypeId = objectTypeId
|
||||
if (objectSchemaId) body.objectSchemaId = objectSchemaId
|
||||
|
||||
const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/object/aql`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('Assets API error running AQL search', { status: response.status, errorText })
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
objects: Array.isArray(data.objectEntries) ? data.objectEntries.map(mapAssetObject) : [],
|
||||
total: data.totalFilterCount ?? (data.objectEntries?.length || 0),
|
||||
pageNumber: data.pageNumber ?? 1,
|
||||
pageSize: data.pageSize ?? (data.objectEntries?.length || 0),
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error running Assets AQL search', { error: toError(error).message })
|
||||
return NextResponse.json(
|
||||
{ error: getErrorMessage(error, 'Internal server error'), success: false },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,134 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmCommentContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmCommentAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmCommentContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: providedCloudId,
|
||||
issueIdOrKey,
|
||||
body: commentBody,
|
||||
isPublic,
|
||||
} = parsed.data.body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!issueIdOrKey) {
|
||||
logger.error('Missing issueIdOrKey in request')
|
||||
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!commentBody) {
|
||||
logger.error('Missing comment body in request')
|
||||
return NextResponse.json({ error: 'Comment body is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = providedCloudId || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
|
||||
if (!issueIdOrKeyValidation.isValid) {
|
||||
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
const url = `${baseUrl}/request/${issueIdOrKey}/comment`
|
||||
|
||||
logger.info('Adding comment to:', url)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
body: JSON.stringify({
|
||||
body: commentBody,
|
||||
public: isPublic ?? true,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueIdOrKey,
|
||||
commentId: data.id,
|
||||
body: data.body,
|
||||
isPublic: data.public,
|
||||
author: data.author
|
||||
? {
|
||||
accountId: data.author.accountId ?? null,
|
||||
displayName: data.author.displayName ?? null,
|
||||
emailAddress: data.author.emailAddress ?? null,
|
||||
}
|
||||
: null,
|
||||
createdDate: data.created ?? null,
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error adding comment:', {
|
||||
error: toError(error).message,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: getErrorMessage(error, 'Internal server error'),
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,126 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmCommentsContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmCommentsAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmCommentsContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: cloudIdParam,
|
||||
issueIdOrKey,
|
||||
isPublic,
|
||||
internal,
|
||||
expand,
|
||||
start,
|
||||
limit,
|
||||
} = parsed.data.body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!issueIdOrKey) {
|
||||
logger.error('Missing issueIdOrKey in request')
|
||||
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
|
||||
if (!issueIdOrKeyValidation.isValid) {
|
||||
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
const params = new URLSearchParams()
|
||||
if (isPublic !== undefined) params.append('public', String(isPublic))
|
||||
if (internal !== undefined) params.append('internal', String(internal))
|
||||
if (expand) params.append('expand', expand)
|
||||
if (start) params.append('start', start)
|
||||
if (limit) params.append('limit', limit)
|
||||
|
||||
const url = `${baseUrl}/request/${issueIdOrKey}/comment${params.toString() ? `?${params.toString()}` : ''}`
|
||||
|
||||
logger.info('Fetching comments from:', url)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueIdOrKey,
|
||||
comments: data.values || [],
|
||||
total: data.size || 0,
|
||||
isLastPage: data.isLastPage ?? true,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error fetching comments:', {
|
||||
error: toError(error).message,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: getErrorMessage(error, 'Internal server error'),
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,186 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmCustomersContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmCustomersAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmCustomersContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: cloudIdParam,
|
||||
serviceDeskId,
|
||||
query,
|
||||
start,
|
||||
limit,
|
||||
accountIds,
|
||||
emails,
|
||||
} = parsed.data.body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!serviceDeskId) {
|
||||
logger.error('Missing serviceDeskId in request')
|
||||
return NextResponse.json({ error: 'Service Desk ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (emails !== undefined) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
'The `emails` parameter is no longer supported. Use `accountIds` (Atlassian account IDs) instead.',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId')
|
||||
if (!serviceDeskIdValidation.isValid) {
|
||||
return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
const splitCsv = (value: unknown): string[] =>
|
||||
value
|
||||
? typeof value === 'string'
|
||||
? value
|
||||
.split(',')
|
||||
.map((v: string) => v.trim())
|
||||
.filter((v: string) => v)
|
||||
: Array.isArray(value)
|
||||
? (value as string[])
|
||||
: []
|
||||
: []
|
||||
|
||||
const parsedAccountIds = splitCsv(accountIds)
|
||||
|
||||
if (parsedAccountIds.length > 0) {
|
||||
const url = `${baseUrl}/servicedesk/${serviceDeskId}/customer`
|
||||
|
||||
logger.info('Adding customers to:', url, {
|
||||
accountIds: parsedAccountIds,
|
||||
})
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
body: JSON.stringify({ accountIds: parsedAccountIds }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
serviceDeskId,
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
const params = new URLSearchParams()
|
||||
if (query) params.append('query', query)
|
||||
if (start) params.append('start', start)
|
||||
if (limit) params.append('limit', limit)
|
||||
|
||||
const url = `${baseUrl}/servicedesk/${serviceDeskId}/customer${params.toString() ? `?${params.toString()}` : ''}`
|
||||
|
||||
logger.info('Fetching customers from:', url)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
customers: data.values || [],
|
||||
total: data.size || 0,
|
||||
isLastPage: data.isLastPage ?? true,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error with customers operation:', {
|
||||
error: toError(error).message,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: getErrorMessage(error, 'Internal server error'),
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,117 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmFormAnswersContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmGetFormAnswersAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmFormAnswersContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey, formId } = parsed.data.body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!issueIdOrKey) {
|
||||
logger.error('Missing issueIdOrKey in request')
|
||||
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!formId) {
|
||||
logger.error('Missing formId in request')
|
||||
return NextResponse.json({ error: 'Form ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
|
||||
if (!issueIdOrKeyValidation.isValid) {
|
||||
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const formIdValidation = validateJiraCloudId(formId, 'formId')
|
||||
if (!formIdValidation.isValid) {
|
||||
return NextResponse.json({ error: formIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
|
||||
const url = `${baseUrl}/issue/${encodeURIComponent(issueIdOrKey)}/form/${encodeURIComponent(formId)}/format/answers`
|
||||
|
||||
logger.info('Getting form answers:', { url, issueIdOrKey, formId })
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM Forms API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueIdOrKey,
|
||||
formId,
|
||||
answers: data ?? null,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error getting form answers:', {
|
||||
error: toError(error).message,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: getErrorMessage(error, 'Internal server error'),
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,131 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmAttachFormContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmAttachFormAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmAttachFormContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: cloudIdParam,
|
||||
issueIdOrKey,
|
||||
formTemplateId,
|
||||
} = parsed.data.body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!issueIdOrKey) {
|
||||
logger.error('Missing issueIdOrKey in request')
|
||||
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!formTemplateId) {
|
||||
logger.error('Missing formTemplateId in request')
|
||||
return NextResponse.json({ error: 'Form template ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
|
||||
if (!issueIdOrKeyValidation.isValid) {
|
||||
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const formTemplateIdValidation = validateJiraCloudId(formTemplateId, 'formTemplateId')
|
||||
if (!formTemplateIdValidation.isValid) {
|
||||
return NextResponse.json({ error: formTemplateIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
|
||||
const url = `${baseUrl}/issue/${encodeURIComponent(issueIdOrKey)}/form`
|
||||
|
||||
logger.info('Attaching form to issue:', { url, issueIdOrKey, formTemplateId })
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
body: JSON.stringify({
|
||||
formTemplate: { id: formTemplateId },
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM Forms API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueIdOrKey,
|
||||
id: data.id ?? null,
|
||||
name: data.name ?? null,
|
||||
updated: data.updated ?? null,
|
||||
submitted: data.submitted ?? false,
|
||||
lock: data.lock ?? false,
|
||||
internal: data.internal ?? null,
|
||||
formTemplateId: (data.formTemplate as Record<string, unknown>)?.id ?? null,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error attaching form:', {
|
||||
error: toError(error).message,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: getErrorMessage(error, 'Internal server error'),
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,132 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmCopyFormsContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmCopyFormsAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmCopyFormsContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: cloudIdParam,
|
||||
sourceIssueIdOrKey,
|
||||
targetIssueIdOrKey,
|
||||
formIds,
|
||||
} = parsed.data.body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!sourceIssueIdOrKey) {
|
||||
logger.error('Missing sourceIssueIdOrKey in request')
|
||||
return NextResponse.json({ error: 'Source issue ID or key is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!targetIssueIdOrKey) {
|
||||
logger.error('Missing targetIssueIdOrKey in request')
|
||||
return NextResponse.json({ error: 'Target issue ID or key is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const sourceValidation = validateJiraIssueKey(sourceIssueIdOrKey, 'sourceIssueIdOrKey')
|
||||
if (!sourceValidation.isValid) {
|
||||
return NextResponse.json({ error: sourceValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const targetValidation = validateJiraIssueKey(targetIssueIdOrKey, 'targetIssueIdOrKey')
|
||||
if (!targetValidation.isValid) {
|
||||
return NextResponse.json({ error: targetValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
|
||||
const url = `${baseUrl}/issue/${encodeURIComponent(sourceIssueIdOrKey)}/form/copy/${encodeURIComponent(targetIssueIdOrKey)}`
|
||||
|
||||
if (formIds !== undefined && !Array.isArray(formIds)) {
|
||||
return NextResponse.json({ error: 'formIds must be an array of form UUIDs' }, { status: 400 })
|
||||
}
|
||||
|
||||
const requestBody = Array.isArray(formIds) && formIds.length > 0 ? { ids: formIds } : {}
|
||||
|
||||
logger.info('Copying forms:', { url, sourceIssueIdOrKey, targetIssueIdOrKey, formIds })
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
body: JSON.stringify(requestBody),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM Forms API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
sourceIssueIdOrKey,
|
||||
targetIssueIdOrKey,
|
||||
copiedForms: data.copiedForms ?? [],
|
||||
errors: data.errors ?? [],
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error copying forms:', {
|
||||
error: toError(error).message,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: getErrorMessage(error, 'Internal server error'),
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,117 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmDeleteFormContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmDeleteFormAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmDeleteFormContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey, formId } = parsed.data.body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!issueIdOrKey) {
|
||||
logger.error('Missing issueIdOrKey in request')
|
||||
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!formId) {
|
||||
logger.error('Missing formId in request')
|
||||
return NextResponse.json({ error: 'Form ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
|
||||
if (!issueIdOrKeyValidation.isValid) {
|
||||
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const formIdValidation = validateJiraCloudId(formId, 'formId')
|
||||
if (!formIdValidation.isValid) {
|
||||
return NextResponse.json({ error: formIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
|
||||
const url = `${baseUrl}/issue/${encodeURIComponent(issueIdOrKey)}/form/${encodeURIComponent(formId)}`
|
||||
|
||||
logger.info('Deleting form:', { url, issueIdOrKey, formId })
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'DELETE',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM Forms API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
await response.text()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueIdOrKey,
|
||||
formId,
|
||||
deleted: true,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error deleting form:', {
|
||||
error: toError(error).message,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: getErrorMessage(error, 'Internal server error'),
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,118 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmExternaliseFormContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmExternaliseFormAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmExternaliseFormContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey, formId } = parsed.data.body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!issueIdOrKey) {
|
||||
logger.error('Missing issueIdOrKey in request')
|
||||
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!formId) {
|
||||
logger.error('Missing formId in request')
|
||||
return NextResponse.json({ error: 'Form ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
|
||||
if (!issueIdOrKeyValidation.isValid) {
|
||||
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const formIdValidation = validateJiraCloudId(formId, 'formId')
|
||||
if (!formIdValidation.isValid) {
|
||||
return NextResponse.json({ error: formIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
|
||||
const url = `${baseUrl}/issue/${encodeURIComponent(issueIdOrKey)}/form/${encodeURIComponent(formId)}/action/external`
|
||||
|
||||
logger.info('Externalising form:', { url, issueIdOrKey, formId })
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM Forms API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const bodyText = await response.text()
|
||||
const data = bodyText ? JSON.parse(bodyText) : {}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueIdOrKey,
|
||||
formId,
|
||||
visibility: data.visibility ?? 'external',
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error externalising form:', {
|
||||
error: toError(error).message,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: getErrorMessage(error, 'Internal server error'),
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,119 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmGetFormContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmGetFormAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmGetFormContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey, formId } = parsed.data.body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!issueIdOrKey) {
|
||||
logger.error('Missing issueIdOrKey in request')
|
||||
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!formId) {
|
||||
logger.error('Missing formId in request')
|
||||
return NextResponse.json({ error: 'Form ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
|
||||
if (!issueIdOrKeyValidation.isValid) {
|
||||
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const formIdValidation = validateJiraCloudId(formId, 'formId')
|
||||
if (!formIdValidation.isValid) {
|
||||
return NextResponse.json({ error: formIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
|
||||
const url = `${baseUrl}/issue/${encodeURIComponent(issueIdOrKey)}/form/${encodeURIComponent(formId)}`
|
||||
|
||||
logger.info('Getting form:', { url, issueIdOrKey, formId })
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM Forms API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueIdOrKey,
|
||||
formId,
|
||||
design: data.design ?? null,
|
||||
state: data.state ?? null,
|
||||
updated: data.updated ?? null,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error getting form:', {
|
||||
error: toError(error).message,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: getErrorMessage(error, 'Internal server error'),
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,118 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmInternaliseFormContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmInternaliseFormAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmInternaliseFormContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey, formId } = parsed.data.body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!issueIdOrKey) {
|
||||
logger.error('Missing issueIdOrKey in request')
|
||||
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!formId) {
|
||||
logger.error('Missing formId in request')
|
||||
return NextResponse.json({ error: 'Form ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
|
||||
if (!issueIdOrKeyValidation.isValid) {
|
||||
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const formIdValidation = validateJiraCloudId(formId, 'formId')
|
||||
if (!formIdValidation.isValid) {
|
||||
return NextResponse.json({ error: formIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
|
||||
const url = `${baseUrl}/issue/${encodeURIComponent(issueIdOrKey)}/form/${encodeURIComponent(formId)}/action/internal`
|
||||
|
||||
logger.info('Internalising form:', { url, issueIdOrKey, formId })
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM Forms API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const bodyText = await response.text()
|
||||
const data = bodyText ? JSON.parse(bodyText) : {}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueIdOrKey,
|
||||
formId,
|
||||
visibility: data.visibility ?? 'internal',
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error internalising form:', {
|
||||
error: toError(error).message,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: getErrorMessage(error, 'Internal server error'),
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,117 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmIssueFormsContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmIssueFormsAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmIssueFormsContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey } = parsed.data.body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!issueIdOrKey) {
|
||||
logger.error('Missing issueIdOrKey in request')
|
||||
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
|
||||
if (!issueIdOrKeyValidation.isValid) {
|
||||
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
|
||||
const url = `${baseUrl}/issue/${encodeURIComponent(issueIdOrKey)}/form`
|
||||
|
||||
logger.info('Fetching issue forms from:', { url, issueIdOrKey })
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM Forms API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
const forms = Array.isArray(data) ? data : (data.values ?? data.forms ?? [])
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueIdOrKey,
|
||||
forms: forms.map((form: Record<string, unknown>) => ({
|
||||
id: form.id ?? null,
|
||||
name: form.name ?? null,
|
||||
updated: form.updated ?? null,
|
||||
submitted: form.submitted ?? false,
|
||||
lock: form.lock ?? false,
|
||||
internal: form.internal ?? null,
|
||||
formTemplateId: (form.formTemplate as Record<string, unknown>)?.id ?? null,
|
||||
})),
|
||||
total: forms.length,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error fetching issue forms:', {
|
||||
error: toError(error).message,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: getErrorMessage(error, 'Internal server error'),
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,118 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmReopenFormContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmReopenFormAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmReopenFormContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey, formId } = parsed.data.body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!issueIdOrKey) {
|
||||
logger.error('Missing issueIdOrKey in request')
|
||||
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!formId) {
|
||||
logger.error('Missing formId in request')
|
||||
return NextResponse.json({ error: 'Form ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
|
||||
if (!issueIdOrKeyValidation.isValid) {
|
||||
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const formIdValidation = validateJiraCloudId(formId, 'formId')
|
||||
if (!formIdValidation.isValid) {
|
||||
return NextResponse.json({ error: formIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
|
||||
const url = `${baseUrl}/issue/${encodeURIComponent(issueIdOrKey)}/form/${encodeURIComponent(formId)}/action/reopen`
|
||||
|
||||
logger.info('Reopening form:', { url, issueIdOrKey, formId })
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM Forms API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const bodyText = await response.text()
|
||||
const data = bodyText ? JSON.parse(bodyText) : {}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueIdOrKey,
|
||||
formId,
|
||||
status: data.status ?? 'open',
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error reopening form:', {
|
||||
error: toError(error).message,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: getErrorMessage(error, 'Internal server error'),
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,131 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmSaveFormAnswersContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmSaveFormAnswersAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmSaveFormAnswersContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: cloudIdParam,
|
||||
issueIdOrKey,
|
||||
formId,
|
||||
answers,
|
||||
} = parsed.data.body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!issueIdOrKey) {
|
||||
logger.error('Missing issueIdOrKey in request')
|
||||
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!formId) {
|
||||
logger.error('Missing formId in request')
|
||||
return NextResponse.json({ error: 'Form ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!answers || typeof answers !== 'object' || Array.isArray(answers)) {
|
||||
logger.error('Missing or invalid answers in request')
|
||||
return NextResponse.json({ error: 'Answers object is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
|
||||
if (!issueIdOrKeyValidation.isValid) {
|
||||
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const formIdValidation = validateJiraCloudId(formId, 'formId')
|
||||
if (!formIdValidation.isValid) {
|
||||
return NextResponse.json({ error: formIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
|
||||
const url = `${baseUrl}/issue/${encodeURIComponent(issueIdOrKey)}/form/${encodeURIComponent(formId)}`
|
||||
|
||||
logger.info('Saving form answers:', { url, issueIdOrKey, formId })
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
body: JSON.stringify({ answers }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM Forms API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueIdOrKey,
|
||||
formId,
|
||||
state: data.state ?? null,
|
||||
updated: data.updated ?? null,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error saving form answers:', {
|
||||
error: toError(error).message,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: getErrorMessage(error, 'Internal server error'),
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,119 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmProjectFormStructureContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmFormStructureAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmProjectFormStructureContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const { domain, accessToken, cloudId: cloudIdParam, projectIdOrKey, formId } = parsed.data.body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!projectIdOrKey) {
|
||||
logger.error('Missing projectIdOrKey in request')
|
||||
return NextResponse.json({ error: 'Project ID or key is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!formId) {
|
||||
logger.error('Missing formId in request')
|
||||
return NextResponse.json({ error: 'Form ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const projectIdOrKeyValidation = validateJiraIssueKey(projectIdOrKey, 'projectIdOrKey')
|
||||
if (!projectIdOrKeyValidation.isValid) {
|
||||
return NextResponse.json({ error: projectIdOrKeyValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const formIdValidation = validateJiraCloudId(formId, 'formId')
|
||||
if (!formIdValidation.isValid) {
|
||||
return NextResponse.json({ error: formIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
|
||||
const url = `${baseUrl}/project/${encodeURIComponent(projectIdOrKey)}/form/${encodeURIComponent(formId)}`
|
||||
|
||||
logger.info('Fetching form template from:', { url, projectIdOrKey, formId })
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM Forms API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
projectIdOrKey,
|
||||
formId,
|
||||
design: data.design ?? null,
|
||||
updated: data.updated ?? null,
|
||||
publish: data.publish ?? null,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error fetching form structure:', {
|
||||
error: toError(error).message,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: getErrorMessage(error, 'Internal server error'),
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,118 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmSubmitFormContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmSubmitFormAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmSubmitFormContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey, formId } = parsed.data.body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!issueIdOrKey) {
|
||||
logger.error('Missing issueIdOrKey in request')
|
||||
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!formId) {
|
||||
logger.error('Missing formId in request')
|
||||
return NextResponse.json({ error: 'Form ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
|
||||
if (!issueIdOrKeyValidation.isValid) {
|
||||
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const formIdValidation = validateJiraCloudId(formId, 'formId')
|
||||
if (!formIdValidation.isValid) {
|
||||
return NextResponse.json({ error: formIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
|
||||
const url = `${baseUrl}/issue/${encodeURIComponent(issueIdOrKey)}/form/${encodeURIComponent(formId)}/action/submit`
|
||||
|
||||
logger.info('Submitting form:', { url, issueIdOrKey, formId })
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM Forms API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const bodyText = await response.text()
|
||||
const data = bodyText ? JSON.parse(bodyText) : {}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueIdOrKey,
|
||||
formId,
|
||||
status: data.status ?? 'submitted',
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error submitting form:', {
|
||||
error: toError(error).message,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: getErrorMessage(error, 'Internal server error'),
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,117 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmProjectFormTemplatesContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmFormTemplatesAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmProjectFormTemplatesContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const { domain, accessToken, cloudId: cloudIdParam, projectIdOrKey } = parsed.data.body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!projectIdOrKey) {
|
||||
logger.error('Missing projectIdOrKey in request')
|
||||
return NextResponse.json({ error: 'Project ID or key is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const projectIdOrKeyValidation = validateJiraIssueKey(projectIdOrKey, 'projectIdOrKey')
|
||||
if (!projectIdOrKeyValidation.isValid) {
|
||||
return NextResponse.json({ error: projectIdOrKeyValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
|
||||
const url = `${baseUrl}/project/${encodeURIComponent(projectIdOrKey)}/form`
|
||||
|
||||
logger.info('Fetching project form templates from:', { url, projectIdOrKey })
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM Forms API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
const templates = Array.isArray(data) ? data : (data.values ?? [])
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
projectIdOrKey,
|
||||
templates: templates.map((template: Record<string, unknown>) => ({
|
||||
id: template.id ?? null,
|
||||
name: template.name ?? null,
|
||||
updated: template.updated ?? null,
|
||||
issueCreateIssueTypeIds: template.issueCreateIssueTypeIds ?? [],
|
||||
issueCreateRequestTypeIds: template.issueCreateRequestTypeIds ?? [],
|
||||
portalRequestTypeIds: template.portalRequestTypeIds ?? [],
|
||||
recommendedIssueRequestTypeIds: template.recommendedIssueRequestTypeIds ?? [],
|
||||
})),
|
||||
total: templates.length,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error fetching form templates:', {
|
||||
error: toError(error).message,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: getErrorMessage(error, 'Internal server error'),
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,198 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmOrganizationContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import {
|
||||
validateAlphanumericId,
|
||||
validateEnum,
|
||||
validateJiraCloudId,
|
||||
} from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmOrganizationAPI')
|
||||
|
||||
const VALID_ACTIONS = ['create', 'add_to_service_desk'] as const
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmOrganizationContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: cloudIdParam,
|
||||
action,
|
||||
name,
|
||||
serviceDeskId,
|
||||
organizationId,
|
||||
} = parsed.data.body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!action) {
|
||||
logger.error('Missing action in request')
|
||||
return NextResponse.json({ error: 'Action is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const actionValidation = validateEnum(action, VALID_ACTIONS, 'action')
|
||||
if (!actionValidation.isValid) {
|
||||
return NextResponse.json({ error: actionValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
if (action === 'create') {
|
||||
if (!name) {
|
||||
logger.error('Missing organization name in request')
|
||||
return NextResponse.json({ error: 'Organization name is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const url = `${baseUrl}/organization`
|
||||
|
||||
logger.info('Creating organization:', { name })
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
body: JSON.stringify({ name }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
organizationId: data.id,
|
||||
name: data.name,
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
if (action === 'add_to_service_desk') {
|
||||
if (!serviceDeskId) {
|
||||
logger.error('Missing serviceDeskId in request')
|
||||
return NextResponse.json({ error: 'Service Desk ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!organizationId) {
|
||||
logger.error('Missing organizationId in request')
|
||||
return NextResponse.json({ error: 'Organization ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId')
|
||||
if (!serviceDeskIdValidation.isValid) {
|
||||
return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const organizationIdValidation = validateAlphanumericId(organizationId, 'organizationId')
|
||||
if (!organizationIdValidation.isValid) {
|
||||
return NextResponse.json({ error: organizationIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const orgIdNumeric = Number.parseInt(String(organizationId).trim(), 10)
|
||||
if (!Number.isFinite(orgIdNumeric) || orgIdNumeric <= 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'organizationId must be a positive integer' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const url = `${baseUrl}/servicedesk/${serviceDeskId}/organization`
|
||||
|
||||
logger.info('Adding organization to service desk:', { serviceDeskId, organizationId })
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
body: JSON.stringify({ organizationId: orgIdNumeric }),
|
||||
})
|
||||
|
||||
if (response.status === 204 || response.ok) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
serviceDeskId,
|
||||
organizationId,
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
|
||||
} catch (error) {
|
||||
logger.error('Error in organization operation:', {
|
||||
error: toError(error).message,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: getErrorMessage(error, 'Internal server error'),
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,119 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmOrganizationsContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmOrganizationsAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmOrganizationsContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: cloudIdParam,
|
||||
serviceDeskId,
|
||||
start,
|
||||
limit,
|
||||
} = parsed.data.body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!serviceDeskId) {
|
||||
logger.error('Missing serviceDeskId in request')
|
||||
return NextResponse.json({ error: 'Service Desk ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId')
|
||||
if (!serviceDeskIdValidation.isValid) {
|
||||
return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
const params = new URLSearchParams()
|
||||
if (start) params.append('start', start)
|
||||
if (limit) params.append('limit', limit)
|
||||
|
||||
const url = `${baseUrl}/servicedesk/${serviceDeskId}/organization${params.toString() ? `?${params.toString()}` : ''}`
|
||||
|
||||
logger.info('Fetching organizations from:', url)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
organizations: data.values || [],
|
||||
total: data.size || 0,
|
||||
isLastPage: data.isLastPage ?? true,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error fetching organizations:', {
|
||||
error: toError(error).message,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: getErrorMessage(error, 'Internal server error'),
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,195 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmParticipantsContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import {
|
||||
validateEnum,
|
||||
validateJiraCloudId,
|
||||
validateJiraIssueKey,
|
||||
} from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmParticipantsAPI')
|
||||
|
||||
const VALID_ACTIONS = ['get', 'add'] as const
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmParticipantsContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: cloudIdParam,
|
||||
action,
|
||||
issueIdOrKey,
|
||||
accountIds,
|
||||
start,
|
||||
limit,
|
||||
} = parsed.data.body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!issueIdOrKey) {
|
||||
logger.error('Missing issueIdOrKey in request')
|
||||
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!action) {
|
||||
logger.error('Missing action in request')
|
||||
return NextResponse.json({ error: 'Action is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const actionValidation = validateEnum(action, VALID_ACTIONS, 'action')
|
||||
if (!actionValidation.isValid) {
|
||||
return NextResponse.json({ error: actionValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
|
||||
if (!issueIdOrKeyValidation.isValid) {
|
||||
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
if (action === 'get') {
|
||||
const params = new URLSearchParams()
|
||||
if (start) params.append('start', start)
|
||||
if (limit) params.append('limit', limit)
|
||||
|
||||
const url = `${baseUrl}/request/${issueIdOrKey}/participant${params.toString() ? `?${params.toString()}` : ''}`
|
||||
|
||||
logger.info('Fetching participants from:', url)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueIdOrKey,
|
||||
participants: data.values || [],
|
||||
total: data.size || 0,
|
||||
isLastPage: data.isLastPage ?? true,
|
||||
},
|
||||
})
|
||||
}
|
||||
if (action === 'add') {
|
||||
if (!accountIds) {
|
||||
logger.error('Missing accountIds in request')
|
||||
return NextResponse.json({ error: 'Account IDs are required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const parsedAccountIds =
|
||||
typeof accountIds === 'string'
|
||||
? accountIds
|
||||
.split(',')
|
||||
.map((id: string) => id.trim())
|
||||
.filter((id: string) => id)
|
||||
: accountIds
|
||||
|
||||
const url = `${baseUrl}/request/${issueIdOrKey}/participant`
|
||||
|
||||
logger.info('Adding participants to:', url, { accountIds: parsedAccountIds })
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
body: JSON.stringify({ accountIds: parsedAccountIds }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueIdOrKey,
|
||||
participants: data.values || [],
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
|
||||
} catch (error) {
|
||||
logger.error('Error in participants operation:', {
|
||||
error: toError(error).message,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: getErrorMessage(error, 'Internal server error'),
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,121 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmQueuesContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmQueuesAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmQueuesContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: cloudIdParam,
|
||||
serviceDeskId,
|
||||
includeCount,
|
||||
start,
|
||||
limit,
|
||||
} = parsed.data.body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!serviceDeskId) {
|
||||
logger.error('Missing serviceDeskId in request')
|
||||
return NextResponse.json({ error: 'Service Desk ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId')
|
||||
if (!serviceDeskIdValidation.isValid) {
|
||||
return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
const params = new URLSearchParams()
|
||||
if (includeCount) params.append('includeCount', includeCount)
|
||||
if (start) params.append('start', start)
|
||||
if (limit) params.append('limit', limit)
|
||||
|
||||
const url = `${baseUrl}/servicedesk/${serviceDeskId}/queue${params.toString() ? `?${params.toString()}` : ''}`
|
||||
|
||||
logger.info('Fetching queues from:', url)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
queues: data.values || [],
|
||||
total: data.size || 0,
|
||||
isLastPage: data.isLastPage ?? true,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error fetching queues:', {
|
||||
error: toError(error).message,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: getErrorMessage(error, 'Internal server error'),
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,271 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmRequestContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import {
|
||||
validateAlphanumericId,
|
||||
validateJiraCloudId,
|
||||
validateJiraIssueKey,
|
||||
} from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmRequestAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmRequestContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: cloudIdParam,
|
||||
issueIdOrKey,
|
||||
serviceDeskId,
|
||||
requestTypeId,
|
||||
summary,
|
||||
description,
|
||||
raiseOnBehalfOf,
|
||||
requestFieldValues,
|
||||
formAnswers,
|
||||
requestParticipants,
|
||||
channel,
|
||||
expand,
|
||||
} = parsed.data.body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
const isCreateOperation = serviceDeskId && requestTypeId && (summary || formAnswers)
|
||||
|
||||
if (isCreateOperation) {
|
||||
const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId')
|
||||
if (!serviceDeskIdValidation.isValid) {
|
||||
return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const requestTypeIdValidation = validateAlphanumericId(requestTypeId, 'requestTypeId')
|
||||
if (!requestTypeIdValidation.isValid) {
|
||||
return NextResponse.json({ error: requestTypeIdValidation.error }, { status: 400 })
|
||||
}
|
||||
const url = `${baseUrl}/request`
|
||||
|
||||
logger.info('Creating request at:', { url, serviceDeskId, requestTypeId })
|
||||
|
||||
const requestBody: Record<string, unknown> = {
|
||||
serviceDeskId,
|
||||
requestTypeId,
|
||||
}
|
||||
|
||||
if (formAnswers && typeof formAnswers === 'object') {
|
||||
// When form answers are provided, use them as the primary data source.
|
||||
// Per Atlassian docs, fields linked to form questions must NOT also appear
|
||||
// in requestFieldValues — doing so causes a 400 error.
|
||||
requestBody.form = { answers: formAnswers }
|
||||
|
||||
// Only include explicit requestFieldValues if the caller provided them
|
||||
// (they know which fields are safe to include alongside form answers).
|
||||
if (requestFieldValues && typeof requestFieldValues === 'object') {
|
||||
requestBody.requestFieldValues = requestFieldValues
|
||||
}
|
||||
} else if (summary || description || requestFieldValues) {
|
||||
const fieldValues =
|
||||
requestFieldValues && typeof requestFieldValues === 'object'
|
||||
? {
|
||||
...(!requestFieldValues.summary && summary ? { summary } : {}),
|
||||
...(!requestFieldValues.description && description ? { description } : {}),
|
||||
...requestFieldValues,
|
||||
}
|
||||
: {
|
||||
...(summary && { summary }),
|
||||
...(description && { description }),
|
||||
}
|
||||
requestBody.requestFieldValues = fieldValues
|
||||
}
|
||||
|
||||
if (raiseOnBehalfOf) {
|
||||
requestBody.raiseOnBehalfOf = raiseOnBehalfOf
|
||||
}
|
||||
if (requestParticipants) {
|
||||
requestBody.requestParticipants = Array.isArray(requestParticipants)
|
||||
? requestParticipants
|
||||
: typeof requestParticipants === 'string'
|
||||
? requestParticipants
|
||||
.split(',')
|
||||
.map((id: string) => id.trim())
|
||||
.filter(Boolean)
|
||||
: []
|
||||
}
|
||||
if (channel) {
|
||||
requestBody.channel = channel
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
body: JSON.stringify(requestBody),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueId: data.issueId,
|
||||
issueKey: data.issueKey,
|
||||
requestTypeId: data.requestTypeId,
|
||||
serviceDeskId: data.serviceDeskId,
|
||||
createdDate: data.createdDate ?? null,
|
||||
currentStatus: data.currentStatus
|
||||
? {
|
||||
status: data.currentStatus.status ?? null,
|
||||
statusCategory: data.currentStatus.statusCategory ?? null,
|
||||
statusDate: data.currentStatus.statusDate ?? null,
|
||||
}
|
||||
: null,
|
||||
reporter: data.reporter
|
||||
? {
|
||||
accountId: data.reporter.accountId ?? null,
|
||||
displayName: data.reporter.displayName ?? null,
|
||||
emailAddress: data.reporter.emailAddress ?? null,
|
||||
}
|
||||
: null,
|
||||
success: true,
|
||||
url: `https://${domain}/browse/${data.issueKey}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
if (!issueIdOrKey) {
|
||||
logger.error('Missing issueIdOrKey in request')
|
||||
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
|
||||
if (!issueIdOrKeyValidation.isValid) {
|
||||
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const params = new URLSearchParams()
|
||||
if (expand) params.append('expand', expand)
|
||||
|
||||
const url = `${baseUrl}/request/${issueIdOrKey}${params.toString() ? `?${params.toString()}` : ''}`
|
||||
|
||||
logger.info('Fetching request from:', url)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueId: data.issueId ?? null,
|
||||
issueKey: data.issueKey ?? null,
|
||||
requestTypeId: data.requestTypeId ?? null,
|
||||
serviceDeskId: data.serviceDeskId ?? null,
|
||||
createdDate: data.createdDate ?? null,
|
||||
currentStatus: data.currentStatus
|
||||
? {
|
||||
status: data.currentStatus.status ?? null,
|
||||
statusCategory: data.currentStatus.statusCategory ?? null,
|
||||
statusDate: data.currentStatus.statusDate ?? null,
|
||||
}
|
||||
: null,
|
||||
reporter: data.reporter
|
||||
? {
|
||||
accountId: data.reporter.accountId ?? null,
|
||||
displayName: data.reporter.displayName ?? null,
|
||||
emailAddress: data.reporter.emailAddress ?? null,
|
||||
active: data.reporter.active ?? true,
|
||||
}
|
||||
: null,
|
||||
requestFieldValues: (data.requestFieldValues ?? []).map((fv: Record<string, unknown>) => ({
|
||||
fieldId: fv.fieldId ?? null,
|
||||
label: fv.label ?? null,
|
||||
value: fv.value ?? null,
|
||||
})),
|
||||
url: `https://${domain}/browse/${data.issueKey}`,
|
||||
request: data,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error with request operation:', {
|
||||
error: toError(error).message,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: getErrorMessage(error, 'Internal server error'),
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,161 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmRequestsContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import {
|
||||
validateAlphanumericId,
|
||||
validateEnum,
|
||||
validateJiraCloudId,
|
||||
} from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmRequestsAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmRequestsContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: cloudIdParam,
|
||||
serviceDeskId,
|
||||
requestOwnership,
|
||||
requestStatus,
|
||||
requestTypeId,
|
||||
searchTerm,
|
||||
expand,
|
||||
start,
|
||||
limit,
|
||||
} = parsed.data.body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
if (serviceDeskId) {
|
||||
const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId')
|
||||
if (!serviceDeskIdValidation.isValid) {
|
||||
return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 })
|
||||
}
|
||||
}
|
||||
|
||||
const VALID_REQUEST_OWNERSHIP = [
|
||||
'OWNED_REQUESTS',
|
||||
'PARTICIPATED_REQUESTS',
|
||||
'APPROVER',
|
||||
'ALL_REQUESTS',
|
||||
] as const
|
||||
const VALID_REQUEST_STATUS = ['OPEN_REQUESTS', 'CLOSED_REQUESTS', 'ALL_REQUESTS'] as const
|
||||
|
||||
if (requestOwnership) {
|
||||
const ownershipValidation = validateEnum(
|
||||
requestOwnership,
|
||||
VALID_REQUEST_OWNERSHIP,
|
||||
'requestOwnership'
|
||||
)
|
||||
if (!ownershipValidation.isValid) {
|
||||
return NextResponse.json({ error: ownershipValidation.error }, { status: 400 })
|
||||
}
|
||||
}
|
||||
|
||||
if (requestStatus) {
|
||||
const statusValidation = validateEnum(requestStatus, VALID_REQUEST_STATUS, 'requestStatus')
|
||||
if (!statusValidation.isValid) {
|
||||
return NextResponse.json({ error: statusValidation.error }, { status: 400 })
|
||||
}
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
const params = new URLSearchParams()
|
||||
if (serviceDeskId) params.append('serviceDeskId', serviceDeskId)
|
||||
if (requestOwnership) {
|
||||
params.append('requestOwnership', requestOwnership)
|
||||
}
|
||||
if (requestStatus) {
|
||||
params.append('requestStatus', requestStatus)
|
||||
}
|
||||
if (requestTypeId) params.append('requestTypeId', requestTypeId)
|
||||
if (searchTerm) params.append('searchTerm', searchTerm)
|
||||
if (expand) params.append('expand', expand)
|
||||
if (start) params.append('start', start)
|
||||
if (limit) params.append('limit', limit)
|
||||
|
||||
const url = `${baseUrl}/request${params.toString() ? `?${params.toString()}` : ''}`
|
||||
|
||||
logger.info('Fetching requests from:', url)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
requests: data.values || [],
|
||||
total: data.size || 0,
|
||||
isLastPage: data.isLastPage ?? true,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error fetching requests:', {
|
||||
error: toError(error).message,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: getErrorMessage(error, 'Internal server error'),
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,135 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmRequestTypeFieldsContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmRequestTypeFieldsAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmRequestTypeFieldsContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: cloudIdParam,
|
||||
serviceDeskId,
|
||||
requestTypeId,
|
||||
} = parsed.data.body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!serviceDeskId) {
|
||||
logger.error('Missing serviceDeskId in request')
|
||||
return NextResponse.json({ error: 'Service Desk ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!requestTypeId) {
|
||||
logger.error('Missing requestTypeId in request')
|
||||
return NextResponse.json({ error: 'Request Type ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId')
|
||||
if (!serviceDeskIdValidation.isValid) {
|
||||
return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const requestTypeIdValidation = validateAlphanumericId(requestTypeId, 'requestTypeId')
|
||||
if (!requestTypeIdValidation.isValid) {
|
||||
return NextResponse.json({ error: requestTypeIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
const url = `${baseUrl}/servicedesk/${serviceDeskId}/requesttype/${requestTypeId}/field`
|
||||
|
||||
logger.info('Fetching request type fields from:', url)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
serviceDeskId,
|
||||
requestTypeId,
|
||||
canAddRequestParticipants: data.canAddRequestParticipants ?? false,
|
||||
canRaiseOnBehalfOf: data.canRaiseOnBehalfOf ?? false,
|
||||
requestTypeFields: (data.requestTypeFields ?? []).map((field: Record<string, unknown>) => ({
|
||||
fieldId: field.fieldId ?? null,
|
||||
name: field.name ?? null,
|
||||
description: field.description ?? null,
|
||||
required: field.required ?? false,
|
||||
visible: field.visible ?? true,
|
||||
validValues: field.validValues ?? [],
|
||||
presetValues: field.presetValues ?? [],
|
||||
defaultValues: field.defaultValues ?? [],
|
||||
jiraSchema: field.jiraSchema ?? null,
|
||||
})),
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error fetching request type fields:', {
|
||||
error: toError(error).message,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: getErrorMessage(error, 'Internal server error'),
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,125 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmRequestTypesContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmRequestTypesAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmRequestTypesContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: cloudIdParam,
|
||||
serviceDeskId,
|
||||
searchQuery,
|
||||
groupId,
|
||||
expand,
|
||||
start,
|
||||
limit,
|
||||
} = parsed.data.body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!serviceDeskId) {
|
||||
logger.error('Missing serviceDeskId in request')
|
||||
return NextResponse.json({ error: 'Service Desk ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId')
|
||||
if (!serviceDeskIdValidation.isValid) {
|
||||
return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
const params = new URLSearchParams()
|
||||
if (searchQuery) params.append('searchQuery', searchQuery)
|
||||
if (groupId) params.append('groupId', groupId)
|
||||
if (expand) params.append('expand', expand)
|
||||
if (start) params.append('start', start)
|
||||
if (limit) params.append('limit', limit)
|
||||
|
||||
const url = `${baseUrl}/servicedesk/${serviceDeskId}/requesttype${params.toString() ? `?${params.toString()}` : ''}`
|
||||
|
||||
logger.info('Fetching request types from:', url)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
requestTypes: data.values || [],
|
||||
total: data.size || 0,
|
||||
isLastPage: data.isLastPage ?? true,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error fetching request types:', {
|
||||
error: toError(error).message,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: getErrorMessage(error, 'Internal server error'),
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,177 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmRequestTypesSelectorContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
const logger = createLogger('JsmSelectorRequestTypesAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const JSM_REQUEST_TYPES_PAGE_SIZE = 100
|
||||
const MAX_JSM_REQUEST_TYPES_PAGES = 50
|
||||
|
||||
interface JsmPagedResponse<T> {
|
||||
values?: T[]
|
||||
isLastPage?: boolean
|
||||
_links?: { next?: string }
|
||||
}
|
||||
|
||||
interface JsmRequestTypeValue {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Drains the offset-paginated JSM `/servicedesk/{id}/requesttype` endpoint,
|
||||
* advancing `start` by the number of rows actually returned until
|
||||
* `isLastPage === true` (or `_links.next` is absent, or a page comes back
|
||||
* empty). Advancing by the real row count — not the requested `limit` —
|
||||
* prevents skipping items if the server returns a short non-final page. Bounded
|
||||
* by `MAX_JSM_REQUEST_TYPES_PAGES`; emits a `logger.warn` and returns the
|
||||
* partial set rather than looping unbounded when the cap is hit.
|
||||
*/
|
||||
async function fetchAllJsmRequestTypes(
|
||||
requestTypeUrl: string,
|
||||
accessToken: string
|
||||
): Promise<{ values: JsmRequestTypeValue[]; lastResponse: Response }> {
|
||||
const values: JsmRequestTypeValue[] = []
|
||||
let start = 0
|
||||
let lastResponse: Response
|
||||
|
||||
for (let page = 0; page < MAX_JSM_REQUEST_TYPES_PAGES; page++) {
|
||||
const url = `${requestTypeUrl}?start=${start}&limit=${JSM_REQUEST_TYPES_PAGE_SIZE}`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
return { values, lastResponse: response }
|
||||
}
|
||||
|
||||
const data = (await response.json()) as JsmPagedResponse<JsmRequestTypeValue>
|
||||
lastResponse = response
|
||||
|
||||
const pageValues = data.values ?? []
|
||||
values.push(...pageValues)
|
||||
|
||||
if (data.isLastPage === true || !data._links?.next || pageValues.length === 0) {
|
||||
return { values, lastResponse }
|
||||
}
|
||||
|
||||
start += pageValues.length
|
||||
|
||||
if (page === MAX_JSM_REQUEST_TYPES_PAGES - 1) {
|
||||
logger.warn('JSM request type list hit pagination cap; list may be incomplete', {
|
||||
pages: MAX_JSM_REQUEST_TYPES_PAGES,
|
||||
collected: values.length,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return { values, lastResponse: lastResponse! }
|
||||
}
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateRequestId()
|
||||
try {
|
||||
const parsed = await parseRequest(jsmRequestTypesSelectorContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const { credential, workflowId, domain, serviceDeskId } = parsed.data.body
|
||||
|
||||
if (!credential) {
|
||||
logger.error('Missing credential in request')
|
||||
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!domain) {
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!serviceDeskId) {
|
||||
return NextResponse.json({ error: 'Service Desk ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId')
|
||||
if (!serviceDeskIdValidation.isValid) {
|
||||
return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const authz = await authorizeCredentialUse(request, {
|
||||
credentialId: credential,
|
||||
workflowId,
|
||||
})
|
||||
if (!authz.ok || !authz.credentialOwnerUserId) {
|
||||
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credential,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
)
|
||||
if (!accessToken) {
|
||||
logger.error('Failed to get access token', {
|
||||
credentialId: credential,
|
||||
userId: authz.credentialOwnerUserId,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Could not retrieve access token', authRequired: true },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const cloudId = await getJiraCloudId(domain, accessToken)
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudIdValidation.sanitized!)
|
||||
const requestTypeUrl = `${baseUrl}/servicedesk/${serviceDeskIdValidation.sanitized}/requesttype`
|
||||
|
||||
const { values, lastResponse } = await fetchAllJsmRequestTypes(requestTypeUrl, accessToken)
|
||||
|
||||
if (!lastResponse.ok) {
|
||||
const errorText = await lastResponse.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: lastResponse.status,
|
||||
statusText: lastResponse.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(
|
||||
lastResponse.status,
|
||||
lastResponse.statusText,
|
||||
errorText
|
||||
),
|
||||
},
|
||||
{ status: lastResponse.status }
|
||||
)
|
||||
}
|
||||
|
||||
const requestTypes = values.map((rt) => ({
|
||||
id: rt.id,
|
||||
name: rt.name,
|
||||
}))
|
||||
|
||||
return NextResponse.json({ requestTypes })
|
||||
} catch (error) {
|
||||
logger.error('Error listing JSM request types:', error)
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,167 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmServiceDesksSelectorContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
const logger = createLogger('JsmSelectorServiceDesksAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const JSM_SERVICE_DESKS_PAGE_SIZE = 100
|
||||
const MAX_JSM_SERVICE_DESKS_PAGES = 50
|
||||
|
||||
interface JsmPagedResponse<T> {
|
||||
values?: T[]
|
||||
isLastPage?: boolean
|
||||
_links?: { next?: string }
|
||||
}
|
||||
|
||||
interface JsmServiceDeskValue {
|
||||
id: string
|
||||
projectName: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Drains the offset-paginated JSM `/servicedesk` endpoint, advancing `start` by
|
||||
* the number of rows actually returned until `isLastPage === true` (or
|
||||
* `_links.next` is absent, or a page comes back empty). Advancing by the real
|
||||
* row count — not the requested `limit` — prevents skipping items if the server
|
||||
* returns a short non-final page. Bounded by `MAX_JSM_SERVICE_DESKS_PAGES`;
|
||||
* emits a `logger.warn` and returns the partial set rather than looping
|
||||
* unbounded when the cap is hit.
|
||||
*/
|
||||
async function fetchAllJsmServiceDesks(
|
||||
baseUrl: string,
|
||||
accessToken: string
|
||||
): Promise<{ values: JsmServiceDeskValue[]; lastResponse: Response }> {
|
||||
const values: JsmServiceDeskValue[] = []
|
||||
let start = 0
|
||||
let lastResponse: Response
|
||||
|
||||
for (let page = 0; page < MAX_JSM_SERVICE_DESKS_PAGES; page++) {
|
||||
const url = `${baseUrl}/servicedesk?start=${start}&limit=${JSM_SERVICE_DESKS_PAGE_SIZE}`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
return { values, lastResponse: response }
|
||||
}
|
||||
|
||||
const data = (await response.json()) as JsmPagedResponse<JsmServiceDeskValue>
|
||||
lastResponse = response
|
||||
|
||||
const pageValues = data.values ?? []
|
||||
values.push(...pageValues)
|
||||
|
||||
if (data.isLastPage === true || !data._links?.next || pageValues.length === 0) {
|
||||
return { values, lastResponse }
|
||||
}
|
||||
|
||||
start += pageValues.length
|
||||
|
||||
if (page === MAX_JSM_SERVICE_DESKS_PAGES - 1) {
|
||||
logger.warn('JSM service desk list hit pagination cap; list may be incomplete', {
|
||||
pages: MAX_JSM_SERVICE_DESKS_PAGES,
|
||||
collected: values.length,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return { values, lastResponse: lastResponse! }
|
||||
}
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateRequestId()
|
||||
try {
|
||||
const parsed = await parseRequest(jsmServiceDesksSelectorContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const { credential, workflowId, domain } = parsed.data.body
|
||||
|
||||
if (!credential) {
|
||||
logger.error('Missing credential in request')
|
||||
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!domain) {
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const authz = await authorizeCredentialUse(request, {
|
||||
credentialId: credential,
|
||||
workflowId,
|
||||
})
|
||||
if (!authz.ok || !authz.credentialOwnerUserId) {
|
||||
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credential,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
)
|
||||
if (!accessToken) {
|
||||
logger.error('Failed to get access token', {
|
||||
credentialId: credential,
|
||||
userId: authz.credentialOwnerUserId,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Could not retrieve access token', authRequired: true },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const cloudId = await getJiraCloudId(domain, accessToken)
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudIdValidation.sanitized!)
|
||||
|
||||
const { values, lastResponse } = await fetchAllJsmServiceDesks(baseUrl, accessToken)
|
||||
|
||||
if (!lastResponse.ok) {
|
||||
const errorText = await lastResponse.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: lastResponse.status,
|
||||
statusText: lastResponse.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(
|
||||
lastResponse.status,
|
||||
lastResponse.statusText,
|
||||
errorText
|
||||
),
|
||||
},
|
||||
{ status: lastResponse.status }
|
||||
)
|
||||
}
|
||||
|
||||
const serviceDesks = values.map((sd) => ({
|
||||
id: sd.id,
|
||||
name: sd.projectName,
|
||||
}))
|
||||
|
||||
return NextResponse.json({ serviceDesks })
|
||||
} catch (error) {
|
||||
logger.error('Error listing JSM service desks:', error)
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,103 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmServiceDesksContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmServiceDesksAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmServiceDesksContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const { domain, accessToken, cloudId: cloudIdParam, expand, start, limit } = parsed.data.body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
const params = new URLSearchParams()
|
||||
if (expand) params.append('expand', expand)
|
||||
if (start) params.append('start', start)
|
||||
if (limit) params.append('limit', limit)
|
||||
|
||||
const url = `${baseUrl}/servicedesk${params.toString() ? `?${params.toString()}` : ''}`
|
||||
|
||||
logger.info('Fetching service desks from:', url)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
serviceDesks: data.values || [],
|
||||
total: data.size || 0,
|
||||
isLastPage: data.isLastPage ?? true,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error fetching service desks:', {
|
||||
error: toError(error).message,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: getErrorMessage(error, 'Internal server error'),
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,120 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmSlaContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmSlaAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmSlaContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: cloudIdParam,
|
||||
issueIdOrKey,
|
||||
start,
|
||||
limit,
|
||||
} = parsed.data.body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!issueIdOrKey) {
|
||||
logger.error('Missing issueIdOrKey in request')
|
||||
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
|
||||
if (!issueIdOrKeyValidation.isValid) {
|
||||
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
const params = new URLSearchParams()
|
||||
if (start) params.append('start', start)
|
||||
if (limit) params.append('limit', limit)
|
||||
|
||||
const url = `${baseUrl}/request/${issueIdOrKey}/sla${params.toString() ? `?${params.toString()}` : ''}`
|
||||
|
||||
logger.info('Fetching SLA info from:', url)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueIdOrKey,
|
||||
slas: data.values || [],
|
||||
total: data.size || 0,
|
||||
isLastPage: data.isLastPage ?? true,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error fetching SLA info:', {
|
||||
error: toError(error).message,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: getErrorMessage(error, 'Internal server error'),
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,138 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmTransitionContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import {
|
||||
validateAlphanumericId,
|
||||
validateJiraCloudId,
|
||||
validateJiraIssueKey,
|
||||
} from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmTransitionAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmTransitionContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: providedCloudId,
|
||||
issueIdOrKey,
|
||||
transitionId,
|
||||
comment,
|
||||
} = parsed.data.body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!issueIdOrKey) {
|
||||
logger.error('Missing issueIdOrKey in request')
|
||||
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!transitionId) {
|
||||
logger.error('Missing transitionId in request')
|
||||
return NextResponse.json({ error: 'Transition ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = providedCloudId || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
|
||||
if (!issueIdOrKeyValidation.isValid) {
|
||||
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const transitionIdValidation = validateAlphanumericId(transitionId, 'transitionId')
|
||||
if (!transitionIdValidation.isValid) {
|
||||
return NextResponse.json({ error: transitionIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
const url = `${baseUrl}/request/${issueIdOrKey}/transition`
|
||||
|
||||
logger.info('Transitioning request at:', url)
|
||||
|
||||
const body: Record<string, unknown> = {
|
||||
id: transitionId,
|
||||
}
|
||||
|
||||
if (comment) {
|
||||
body.additionalComment = {
|
||||
body: comment,
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueIdOrKey,
|
||||
transitionId,
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error transitioning request:', {
|
||||
error: toError(error).message,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: getErrorMessage(error, 'Internal server error'),
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,120 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { jsmTransitionsContract } from '@/lib/api/contracts/selectors/jsm'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
import { getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmTransitionsAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(jsmTransitionsContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: cloudIdParam,
|
||||
issueIdOrKey,
|
||||
start,
|
||||
limit,
|
||||
} = parsed.data.body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!issueIdOrKey) {
|
||||
logger.error('Missing issueIdOrKey in request')
|
||||
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
|
||||
if (!issueIdOrKeyValidation.isValid) {
|
||||
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
const params = new URLSearchParams()
|
||||
if (start) params.append('start', start)
|
||||
if (limit) params.append('limit', limit)
|
||||
|
||||
const url = `${baseUrl}/request/${issueIdOrKey}/transition${params.toString() ? `?${params.toString()}` : ''}`
|
||||
|
||||
logger.info('Fetching transitions from:', url)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueIdOrKey,
|
||||
transitions: data.values || [],
|
||||
total: data.size || 0,
|
||||
isLastPage: data.isLastPage ?? true,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error fetching transitions:', {
|
||||
error: toError(error).message,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: getErrorMessage(error, 'Internal server error'),
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user