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,106 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { asanaAddCommentContract } from '@/lib/api/contracts/tools/asana'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('AsanaAddCommentAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const parsed = await parseRequest(asanaAddCommentContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
const { accessToken, taskGid, text } = parsed.data.body
|
||||
|
||||
const taskGidValidation = validateAlphanumericId(taskGid, 'taskGid', 100)
|
||||
if (!taskGidValidation.isValid) {
|
||||
return NextResponse.json({ error: taskGidValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const url = `https://app.asana.com/api/1.0/tasks/${taskGid}/stories`
|
||||
|
||||
const body = {
|
||||
data: {
|
||||
text,
|
||||
},
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
|
||||
|
||||
try {
|
||||
const errorData = JSON.parse(errorText)
|
||||
const asanaError = errorData.errors?.[0]
|
||||
if (asanaError) {
|
||||
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
|
||||
}
|
||||
logger.error('Asana API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorData,
|
||||
})
|
||||
} catch (_e) {
|
||||
logger.error('Asana API error (unparsed):', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
const story = result.data
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
ts: new Date().toISOString(),
|
||||
gid: story.gid,
|
||||
text: story.text || '',
|
||||
created_at: story.created_at,
|
||||
created_by: story.created_by
|
||||
? {
|
||||
gid: story.created_by.gid,
|
||||
name: story.created_by.name,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error processing request:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to add comment to Asana task',
|
||||
details: (error as Error).message,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,103 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { asanaAddFollowersContract } from '@/lib/api/contracts/tools/asana'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('AsanaAddFollowersAPI')
|
||||
|
||||
interface AsanaFollower {
|
||||
gid: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const parsed = await parseRequest(asanaAddFollowersContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
const { accessToken, taskGid, followers } = parsed.data.body
|
||||
|
||||
const taskGidValidation = validateAlphanumericId(taskGid, 'taskGid', 100)
|
||||
if (!taskGidValidation.isValid) {
|
||||
return NextResponse.json({ error: taskGidValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
for (const follower of followers) {
|
||||
const followerValidation = validateAlphanumericId(follower, 'follower', 100)
|
||||
if (!followerValidation.isValid) {
|
||||
return NextResponse.json({ error: followerValidation.error }, { status: 400 })
|
||||
}
|
||||
}
|
||||
|
||||
const url = `https://app.asana.com/api/1.0/tasks/${taskGid}/addFollowers?opt_fields=name,followers.name`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ data: { followers } }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
|
||||
|
||||
try {
|
||||
const errorData = JSON.parse(errorText)
|
||||
const asanaError = errorData.errors?.[0]
|
||||
if (asanaError) {
|
||||
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
|
||||
}
|
||||
logger.error('Asana API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorData,
|
||||
})
|
||||
} catch (_e) {
|
||||
logger.error('Asana API error (unparsed):', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: errorMessage, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
const task = result.data
|
||||
const taskFollowers: AsanaFollower[] = Array.isArray(task.followers) ? task.followers : []
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
ts: new Date().toISOString(),
|
||||
gid: task.gid,
|
||||
name: task.name || '',
|
||||
followers: taskFollowers.map((follower) => ({
|
||||
gid: follower.gid,
|
||||
name: follower.name,
|
||||
})),
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error adding followers to Asana task:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to add followers to Asana task', details: (error as Error).message },
|
||||
{ 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 { asanaCreateProjectContract } from '@/lib/api/contracts/tools/asana'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('AsanaCreateProjectAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const parsed = await parseRequest(asanaCreateProjectContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
const { accessToken, workspace, name, notes } = parsed.data.body
|
||||
|
||||
const workspaceValidation = validateAlphanumericId(workspace, 'workspace', 100)
|
||||
if (!workspaceValidation.isValid) {
|
||||
return NextResponse.json({ error: workspaceValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const projectData: Record<string, unknown> = { name, workspace }
|
||||
if (notes) {
|
||||
projectData.notes = notes
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
'https://app.asana.com/api/1.0/projects?opt_fields=name,notes,archived,color,created_at,modified_at,permalink_url',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ data: projectData }),
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
|
||||
|
||||
try {
|
||||
const errorData = JSON.parse(errorText)
|
||||
const asanaError = errorData.errors?.[0]
|
||||
if (asanaError) {
|
||||
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
|
||||
}
|
||||
logger.error('Asana API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorData,
|
||||
})
|
||||
} catch (_e) {
|
||||
logger.error('Asana API error (unparsed):', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: errorMessage, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
const project = result.data
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
ts: new Date().toISOString(),
|
||||
gid: project.gid,
|
||||
name: project.name,
|
||||
notes: project.notes || '',
|
||||
archived: project.archived ?? false,
|
||||
color: project.color ?? null,
|
||||
created_at: project.created_at,
|
||||
modified_at: project.modified_at,
|
||||
permalink_url: project.permalink_url,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error creating Asana project:', {
|
||||
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,92 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { asanaCreateSectionContract } from '@/lib/api/contracts/tools/asana'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('AsanaCreateSectionAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const parsed = await parseRequest(asanaCreateSectionContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
const { accessToken, projectGid, name } = parsed.data.body
|
||||
|
||||
const projectGidValidation = validateAlphanumericId(projectGid, 'projectGid', 100)
|
||||
if (!projectGidValidation.isValid) {
|
||||
return NextResponse.json({ error: projectGidValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const url = `https://app.asana.com/api/1.0/projects/${projectGid}/sections`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ data: { name } }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
|
||||
|
||||
try {
|
||||
const errorData = JSON.parse(errorText)
|
||||
const asanaError = errorData.errors?.[0]
|
||||
if (asanaError) {
|
||||
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
|
||||
}
|
||||
logger.error('Asana API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorData,
|
||||
})
|
||||
} catch (_e) {
|
||||
logger.error('Asana API error (unparsed):', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: errorMessage, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
const section = result.data
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
ts: new Date().toISOString(),
|
||||
gid: section.gid,
|
||||
name: section.name,
|
||||
created_at: section.created_at,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error creating Asana section:', {
|
||||
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,106 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { asanaCreateSubtaskContract } from '@/lib/api/contracts/tools/asana'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('AsanaCreateSubtaskAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const parsed = await parseRequest(asanaCreateSubtaskContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
const { accessToken, taskGid, name, notes, assignee, due_on } = parsed.data.body
|
||||
|
||||
const taskGidValidation = validateAlphanumericId(taskGid, 'taskGid', 100)
|
||||
if (!taskGidValidation.isValid) {
|
||||
return NextResponse.json({ error: taskGidValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const subtaskData: Record<string, unknown> = { name }
|
||||
if (notes) {
|
||||
subtaskData.notes = notes
|
||||
}
|
||||
if (assignee) {
|
||||
subtaskData.assignee = assignee
|
||||
}
|
||||
if (due_on) {
|
||||
subtaskData.due_on = due_on
|
||||
}
|
||||
|
||||
const url = `https://app.asana.com/api/1.0/tasks/${taskGid}/subtasks?opt_fields=name,notes,completed,created_at,permalink_url`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ data: subtaskData }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
|
||||
|
||||
try {
|
||||
const errorData = JSON.parse(errorText)
|
||||
const asanaError = errorData.errors?.[0]
|
||||
if (asanaError) {
|
||||
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
|
||||
}
|
||||
logger.error('Asana API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorData,
|
||||
})
|
||||
} catch (_e) {
|
||||
logger.error('Asana API error (unparsed):', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: errorMessage, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
const task = result.data
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
ts: new Date().toISOString(),
|
||||
gid: task.gid,
|
||||
name: task.name,
|
||||
notes: task.notes || '',
|
||||
completed: task.completed || false,
|
||||
created_at: task.created_at,
|
||||
permalink_url: task.permalink_url,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error creating Asana subtask:', {
|
||||
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,122 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { asanaCreateTaskContract } from '@/lib/api/contracts/tools/asana'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('AsanaCreateTaskAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const parsed = await parseRequest(asanaCreateTaskContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
const { accessToken, workspace, name, notes, assignee, due_on } = parsed.data.body
|
||||
|
||||
const workspaceValidation = validateAlphanumericId(workspace, 'workspace', 100)
|
||||
if (!workspaceValidation.isValid) {
|
||||
return NextResponse.json({ error: workspaceValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const url =
|
||||
'https://app.asana.com/api/1.0/tasks?opt_fields=name,notes,completed,created_at,permalink_url'
|
||||
|
||||
const taskData: Record<string, unknown> = {
|
||||
name,
|
||||
workspace,
|
||||
}
|
||||
|
||||
if (notes) {
|
||||
taskData.notes = notes
|
||||
}
|
||||
|
||||
if (assignee) {
|
||||
taskData.assignee = assignee
|
||||
}
|
||||
|
||||
if (due_on) {
|
||||
taskData.due_on = due_on
|
||||
}
|
||||
|
||||
const body = { data: taskData }
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
|
||||
|
||||
try {
|
||||
const errorData = JSON.parse(errorText)
|
||||
const asanaError = errorData.errors?.[0]
|
||||
if (asanaError) {
|
||||
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
|
||||
}
|
||||
logger.error('Asana API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorData,
|
||||
})
|
||||
} catch (_e) {
|
||||
logger.error('Asana API error (unparsed):', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
const task = result.data
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
ts: new Date().toISOString(),
|
||||
gid: task.gid,
|
||||
name: task.name,
|
||||
notes: task.notes || '',
|
||||
completed: task.completed || false,
|
||||
created_at: task.created_at,
|
||||
permalink_url: task.permalink_url,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error creating Asana task:', {
|
||||
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,81 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { asanaDeleteTaskContract } from '@/lib/api/contracts/tools/asana'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('AsanaDeleteTaskAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const parsed = await parseRequest(asanaDeleteTaskContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
const { accessToken, taskGid } = parsed.data.body
|
||||
|
||||
const taskGidValidation = validateAlphanumericId(taskGid, 'taskGid', 100)
|
||||
if (!taskGidValidation.isValid) {
|
||||
return NextResponse.json({ error: taskGidValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const url = `https://app.asana.com/api/1.0/tasks/${taskGid}`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
|
||||
|
||||
try {
|
||||
const errorData = JSON.parse(errorText)
|
||||
const asanaError = errorData.errors?.[0]
|
||||
if (asanaError) {
|
||||
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
|
||||
}
|
||||
logger.error('Asana API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorData,
|
||||
})
|
||||
} catch (_e) {
|
||||
logger.error('Asana API error (unparsed):', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: errorMessage, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
ts: new Date().toISOString(),
|
||||
gid: taskGid,
|
||||
deleted: true,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error deleting Asana task:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to delete Asana task', details: (error as Error).message },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,92 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { asanaGetProjectContract } from '@/lib/api/contracts/tools/asana'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('AsanaGetProjectAPI')
|
||||
|
||||
const PROJECT_OPT_FIELDS = 'name,notes,archived,color,created_at,modified_at,permalink_url'
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const parsed = await parseRequest(asanaGetProjectContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
const { accessToken, projectGid } = parsed.data.body
|
||||
|
||||
const projectGidValidation = validateAlphanumericId(projectGid, 'projectGid', 100)
|
||||
if (!projectGidValidation.isValid) {
|
||||
return NextResponse.json({ error: projectGidValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const url = `https://app.asana.com/api/1.0/projects/${projectGid}?opt_fields=${PROJECT_OPT_FIELDS}`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
|
||||
|
||||
try {
|
||||
const errorData = JSON.parse(errorText)
|
||||
const asanaError = errorData.errors?.[0]
|
||||
if (asanaError) {
|
||||
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
|
||||
}
|
||||
logger.error('Asana API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorData,
|
||||
})
|
||||
} catch (_e) {
|
||||
logger.error('Asana API error (unparsed):', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: errorMessage, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
const project = result.data
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
ts: new Date().toISOString(),
|
||||
gid: project.gid,
|
||||
name: project.name,
|
||||
notes: project.notes || '',
|
||||
archived: project.archived ?? false,
|
||||
color: project.color ?? null,
|
||||
created_at: project.created_at,
|
||||
modified_at: project.modified_at,
|
||||
permalink_url: project.permalink_url,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error processing request:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to retrieve Asana project', details: (error as Error).message },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,94 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { asanaGetProjectsContract } from '@/lib/api/contracts/tools/asana'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('AsanaGetProjectsAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const parsed = await parseRequest(asanaGetProjectsContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
const { accessToken, workspace } = parsed.data.body
|
||||
|
||||
const workspaceValidation = validateAlphanumericId(workspace, 'workspace', 100)
|
||||
if (!workspaceValidation.isValid) {
|
||||
return NextResponse.json({ error: workspaceValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const url = `https://app.asana.com/api/1.0/projects?workspace=${workspace}`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
|
||||
|
||||
try {
|
||||
const errorData = JSON.parse(errorText)
|
||||
const asanaError = errorData.errors?.[0]
|
||||
if (asanaError) {
|
||||
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
|
||||
}
|
||||
logger.error('Asana API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorData,
|
||||
})
|
||||
} catch (_e) {
|
||||
logger.error('Asana API error (unparsed):', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
const projects = result.data
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
ts: new Date().toISOString(),
|
||||
projects: projects.map((project: { gid: string; name: string; resource_type: string }) => ({
|
||||
gid: project.gid,
|
||||
name: project.name,
|
||||
resource_type: project.resource_type,
|
||||
})),
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error processing request:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to retrieve Asana projects',
|
||||
details: (error as Error).message,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,224 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { asanaGetTaskContract } from '@/lib/api/contracts/tools/asana'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('AsanaGetTaskAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const parsed = await parseRequest(asanaGetTaskContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
const { accessToken, taskGid, workspace, project, limit } = parsed.data.body
|
||||
|
||||
if (taskGid) {
|
||||
const taskGidValidation = validateAlphanumericId(taskGid, 'taskGid', 100)
|
||||
if (!taskGidValidation.isValid) {
|
||||
return NextResponse.json({ error: taskGidValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const url = `https://app.asana.com/api/1.0/tasks/${taskGid}?opt_fields=gid,name,notes,completed,assignee,assignee.name,due_on,created_at,modified_at,created_by,created_by.name,resource_type,resource_subtype`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
|
||||
|
||||
try {
|
||||
const errorData = JSON.parse(errorText)
|
||||
const asanaError = errorData.errors?.[0]
|
||||
if (asanaError) {
|
||||
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
|
||||
}
|
||||
logger.error('Asana API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorData,
|
||||
})
|
||||
} catch (_e) {
|
||||
logger.error('Asana API error (unparsed):', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
const task = result.data
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
ts: new Date().toISOString(),
|
||||
gid: task.gid,
|
||||
resource_type: task.resource_type,
|
||||
resource_subtype: task.resource_subtype,
|
||||
name: task.name,
|
||||
notes: task.notes || '',
|
||||
completed: task.completed || false,
|
||||
assignee: task.assignee
|
||||
? {
|
||||
gid: task.assignee.gid,
|
||||
name: task.assignee.name,
|
||||
}
|
||||
: undefined,
|
||||
created_by: task.created_by
|
||||
? {
|
||||
gid: task.created_by.gid,
|
||||
resource_type: task.created_by.resource_type,
|
||||
name: task.created_by.name,
|
||||
}
|
||||
: undefined,
|
||||
due_on: task.due_on || undefined,
|
||||
created_at: task.created_at,
|
||||
modified_at: task.modified_at,
|
||||
})
|
||||
}
|
||||
|
||||
if (!workspace && !project) {
|
||||
logger.error('Either taskGid or workspace/project must be provided')
|
||||
return NextResponse.json(
|
||||
{ error: 'Either taskGid or workspace/project must be provided' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const params = new URLSearchParams()
|
||||
|
||||
if (project) {
|
||||
const projectValidation = validateAlphanumericId(project, 'project', 100)
|
||||
if (!projectValidation.isValid) {
|
||||
return NextResponse.json({ error: projectValidation.error }, { status: 400 })
|
||||
}
|
||||
params.append('project', project)
|
||||
} else if (workspace) {
|
||||
const workspaceValidation = validateAlphanumericId(workspace, 'workspace', 100)
|
||||
if (!workspaceValidation.isValid) {
|
||||
return NextResponse.json({ error: workspaceValidation.error }, { status: 400 })
|
||||
}
|
||||
params.append('workspace', workspace)
|
||||
}
|
||||
|
||||
if (limit) {
|
||||
params.append('limit', String(limit))
|
||||
} else {
|
||||
params.append('limit', '50')
|
||||
}
|
||||
|
||||
params.append(
|
||||
'opt_fields',
|
||||
'gid,name,notes,completed,assignee,assignee.name,due_on,created_at,modified_at,created_by,created_by.name,resource_type,resource_subtype'
|
||||
)
|
||||
|
||||
const url = `https://app.asana.com/api/1.0/tasks?${params.toString()}`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
|
||||
|
||||
try {
|
||||
const errorData = JSON.parse(errorText)
|
||||
const asanaError = errorData.errors?.[0]
|
||||
if (asanaError) {
|
||||
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
|
||||
}
|
||||
logger.error('Asana API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorData,
|
||||
})
|
||||
} catch (_e) {
|
||||
logger.error('Asana API error (unparsed):', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
const tasks = result.data
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
ts: new Date().toISOString(),
|
||||
tasks: tasks.map((task: any) => ({
|
||||
gid: task.gid,
|
||||
resource_type: task.resource_type,
|
||||
resource_subtype: task.resource_subtype,
|
||||
name: task.name,
|
||||
notes: task.notes || '',
|
||||
completed: task.completed || false,
|
||||
assignee: task.assignee
|
||||
? {
|
||||
gid: task.assignee.gid,
|
||||
name: task.assignee.name,
|
||||
}
|
||||
: undefined,
|
||||
created_by: task.created_by
|
||||
? {
|
||||
gid: task.created_by.gid,
|
||||
resource_type: task.created_by.resource_type,
|
||||
name: task.created_by.name,
|
||||
}
|
||||
: undefined,
|
||||
due_on: task.due_on || undefined,
|
||||
created_at: task.created_at,
|
||||
modified_at: task.modified_at,
|
||||
})),
|
||||
next_page: result.next_page,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error processing request:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to retrieve Asana task(s)',
|
||||
details: (error as Error).message,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,93 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { asanaListSectionsContract } from '@/lib/api/contracts/tools/asana'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('AsanaListSectionsAPI')
|
||||
|
||||
interface AsanaSection {
|
||||
gid: string
|
||||
name: string
|
||||
resource_type?: string
|
||||
}
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const parsed = await parseRequest(asanaListSectionsContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
const { accessToken, projectGid } = parsed.data.body
|
||||
|
||||
const projectGidValidation = validateAlphanumericId(projectGid, 'projectGid', 100)
|
||||
if (!projectGidValidation.isValid) {
|
||||
return NextResponse.json({ error: projectGidValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const url = `https://app.asana.com/api/1.0/projects/${projectGid}/sections`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
|
||||
|
||||
try {
|
||||
const errorData = JSON.parse(errorText)
|
||||
const asanaError = errorData.errors?.[0]
|
||||
if (asanaError) {
|
||||
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
|
||||
}
|
||||
logger.error('Asana API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorData,
|
||||
})
|
||||
} catch (_e) {
|
||||
logger.error('Asana API error (unparsed):', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: errorMessage, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
const sections: AsanaSection[] = Array.isArray(result.data) ? result.data : []
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
ts: new Date().toISOString(),
|
||||
sections: sections.map((section) => ({
|
||||
gid: section.gid,
|
||||
name: section.name,
|
||||
resource_type: section.resource_type,
|
||||
})),
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error processing request:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to retrieve Asana sections', details: (error as Error).message },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,87 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { asanaListWorkspacesContract } from '@/lib/api/contracts/tools/asana'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('AsanaListWorkspacesAPI')
|
||||
|
||||
interface AsanaWorkspace {
|
||||
gid: string
|
||||
name: string
|
||||
resource_type?: string
|
||||
}
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const parsed = await parseRequest(asanaListWorkspacesContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
const { accessToken } = parsed.data.body
|
||||
|
||||
const url = 'https://app.asana.com/api/1.0/workspaces?limit=100'
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
|
||||
|
||||
try {
|
||||
const errorData = JSON.parse(errorText)
|
||||
const asanaError = errorData.errors?.[0]
|
||||
if (asanaError) {
|
||||
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
|
||||
}
|
||||
logger.error('Asana API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorData,
|
||||
})
|
||||
} catch (_e) {
|
||||
logger.error('Asana API error (unparsed):', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: errorMessage, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
const workspaces: AsanaWorkspace[] = Array.isArray(result.data) ? result.data : []
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
ts: new Date().toISOString(),
|
||||
workspaces: workspaces.map((workspace) => ({
|
||||
gid: workspace.gid,
|
||||
name: workspace.name,
|
||||
resource_type: workspace.resource_type,
|
||||
})),
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error processing request:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to retrieve Asana workspaces', details: (error as Error).message },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,137 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { asanaSearchTasksContract } from '@/lib/api/contracts/tools/asana'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('AsanaSearchTasksAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const parsed = await parseRequest(asanaSearchTasksContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
const { accessToken, workspace, text, assignee, projects, completed } = parsed.data.body
|
||||
|
||||
const workspaceValidation = validateAlphanumericId(workspace, 'workspace', 100)
|
||||
if (!workspaceValidation.isValid) {
|
||||
return NextResponse.json({ error: workspaceValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const params = new URLSearchParams()
|
||||
|
||||
if (text) {
|
||||
params.append('text', text)
|
||||
}
|
||||
|
||||
if (assignee) {
|
||||
params.append('assignee.any', assignee)
|
||||
}
|
||||
|
||||
if (projects && Array.isArray(projects) && projects.length > 0) {
|
||||
params.append('projects.any', projects.join(','))
|
||||
}
|
||||
|
||||
if (completed !== undefined) {
|
||||
params.append('completed', String(completed))
|
||||
}
|
||||
|
||||
params.append(
|
||||
'opt_fields',
|
||||
'gid,name,notes,completed,assignee,assignee.name,due_on,created_at,modified_at,created_by,created_by.name,resource_type,resource_subtype'
|
||||
)
|
||||
|
||||
const url = `https://app.asana.com/api/1.0/workspaces/${workspace}/tasks/search?${params.toString()}`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
|
||||
|
||||
try {
|
||||
const errorData = JSON.parse(errorText)
|
||||
const asanaError = errorData.errors?.[0]
|
||||
if (asanaError) {
|
||||
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
|
||||
}
|
||||
logger.error('Asana API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorData,
|
||||
})
|
||||
} catch (_e) {
|
||||
logger.error('Asana API error (unparsed):', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
const tasks = result.data
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
ts: new Date().toISOString(),
|
||||
tasks: tasks.map((task: any) => ({
|
||||
gid: task.gid,
|
||||
resource_type: task.resource_type,
|
||||
resource_subtype: task.resource_subtype,
|
||||
name: task.name,
|
||||
notes: task.notes || '',
|
||||
completed: task.completed || false,
|
||||
assignee: task.assignee
|
||||
? {
|
||||
gid: task.assignee.gid,
|
||||
name: task.assignee.name,
|
||||
}
|
||||
: undefined,
|
||||
created_by: task.created_by
|
||||
? {
|
||||
gid: task.created_by.gid,
|
||||
resource_type: task.created_by.resource_type,
|
||||
name: task.created_by.name,
|
||||
}
|
||||
: undefined,
|
||||
due_on: task.due_on || undefined,
|
||||
created_at: task.created_at,
|
||||
modified_at: task.modified_at,
|
||||
})),
|
||||
next_page: result.next_page,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error processing request:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to search Asana tasks',
|
||||
details: (error as Error).message,
|
||||
},
|
||||
{ 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 { asanaUpdateTaskContract } from '@/lib/api/contracts/tools/asana'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('AsanaUpdateTaskAPI')
|
||||
|
||||
export const PUT = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const parsed = await parseRequest(asanaUpdateTaskContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
const { accessToken, taskGid, name, notes, assignee, completed, due_on } = parsed.data.body
|
||||
|
||||
const taskGidValidation = validateAlphanumericId(taskGid, 'taskGid', 100)
|
||||
if (!taskGidValidation.isValid) {
|
||||
return NextResponse.json({ error: taskGidValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const url = `https://app.asana.com/api/1.0/tasks/${taskGid}`
|
||||
|
||||
const taskData: Record<string, unknown> = {}
|
||||
|
||||
if (name !== undefined) {
|
||||
taskData.name = name
|
||||
}
|
||||
|
||||
if (notes !== undefined) {
|
||||
taskData.notes = notes
|
||||
}
|
||||
|
||||
if (assignee !== undefined) {
|
||||
taskData.assignee = assignee
|
||||
}
|
||||
|
||||
if (completed !== undefined) {
|
||||
taskData.completed = completed
|
||||
}
|
||||
|
||||
if (due_on !== undefined) {
|
||||
taskData.due_on = due_on
|
||||
}
|
||||
|
||||
const body = { data: taskData }
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
|
||||
|
||||
try {
|
||||
const errorData = JSON.parse(errorText)
|
||||
const asanaError = errorData.errors?.[0]
|
||||
if (asanaError) {
|
||||
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
|
||||
}
|
||||
logger.error('Asana API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorData,
|
||||
})
|
||||
} catch (_e) {
|
||||
logger.error('Asana API error (unparsed):', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
const task = result.data
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
ts: new Date().toISOString(),
|
||||
gid: task.gid,
|
||||
name: task.name,
|
||||
notes: task.notes || '',
|
||||
completed: task.completed || false,
|
||||
modified_at: task.modified_at,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error updating Asana task:', {
|
||||
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,150 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { asanaWorkspacesSelectorContract } from '@/lib/api/contracts/selectors'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
|
||||
const logger = createLogger('AsanaWorkspacesAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const ASANA_PAGE_LIMIT = 100
|
||||
const ASANA_MAX_WORKSPACES_PAGES = 50
|
||||
|
||||
interface AsanaWorkspace {
|
||||
gid: string
|
||||
name: string
|
||||
}
|
||||
|
||||
interface AsanaWorkspacesPage {
|
||||
data?: AsanaWorkspace[]
|
||||
next_page?: {
|
||||
offset?: string
|
||||
} | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all Asana workspaces using `limit`/`offset` pagination, following
|
||||
* `next_page.offset` (an opaque token, passed back verbatim as `?offset=`)
|
||||
* until `next_page` is null so the full set is returned. Bounded by
|
||||
* `ASANA_MAX_WORKSPACES_PAGES`; logs a warning rather than silently dropping
|
||||
* workspaces when the cap is hit.
|
||||
*/
|
||||
async function fetchAllWorkspaces(accessToken: string): Promise<AsanaWorkspace[]> {
|
||||
const workspaces: AsanaWorkspace[] = []
|
||||
let offset: string | undefined
|
||||
|
||||
for (let page = 0; page < ASANA_MAX_WORKSPACES_PAGES; page++) {
|
||||
const url = new URL('https://app.asana.com/api/1.0/workspaces')
|
||||
url.searchParams.set('limit', String(ASANA_PAGE_LIMIT))
|
||||
if (offset) {
|
||||
url.searchParams.set('offset', offset)
|
||||
}
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
throw new AsanaFetchError(response.status, errorData)
|
||||
}
|
||||
|
||||
const data = (await response.json()) as AsanaWorkspacesPage
|
||||
if (Array.isArray(data.data)) {
|
||||
workspaces.push(...data.data)
|
||||
}
|
||||
|
||||
offset = data.next_page?.offset || undefined
|
||||
if (!offset) {
|
||||
return workspaces
|
||||
}
|
||||
|
||||
if (page === ASANA_MAX_WORKSPACES_PAGES - 1) {
|
||||
logger.warn('Asana workspaces listing hit pagination cap; workspace list may be incomplete', {
|
||||
pages: ASANA_MAX_WORKSPACES_PAGES,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return workspaces
|
||||
}
|
||||
|
||||
class AsanaFetchError extends Error {
|
||||
constructor(
|
||||
readonly status: number,
|
||||
readonly details: unknown
|
||||
) {
|
||||
super('Failed to fetch Asana workspaces')
|
||||
this.name = 'AsanaFetchError'
|
||||
}
|
||||
}
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateRequestId()
|
||||
try {
|
||||
const parsed = await parseRequest(asanaWorkspacesSelectorContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
const { credential, workflowId } = parsed.data.body
|
||||
|
||||
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 }
|
||||
)
|
||||
}
|
||||
|
||||
let allWorkspaces: AsanaWorkspace[]
|
||||
try {
|
||||
allWorkspaces = await fetchAllWorkspaces(accessToken)
|
||||
} catch (error) {
|
||||
if (error instanceof AsanaFetchError) {
|
||||
logger.error('Failed to fetch Asana workspaces', {
|
||||
status: error.status,
|
||||
error: error.details,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch Asana workspaces', details: error.details },
|
||||
{ status: error.status }
|
||||
)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
const workspaces = allWorkspaces.map((workspace) => ({
|
||||
id: workspace.gid,
|
||||
name: workspace.name,
|
||||
}))
|
||||
|
||||
return NextResponse.json({ workspaces })
|
||||
} catch (error) {
|
||||
logger.error('Error processing Asana workspaces request:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to retrieve Asana workspaces', details: (error as Error).message },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user