improvement(oauth): refactor oauth apis to dedicated tools routes, fix docs (#423)

* migrate oauth apis to dedicated tools routes

* added tests

* fix docs

* fix confluence file selector
This commit is contained in:
Waleed Latif
2025-05-26 12:00:03 -07:00
committed by GitHub
parent 8e6057a39e
commit 6afb453fc0
37 changed files with 1238 additions and 152 deletions

View File

@@ -1,172 +0,0 @@
import { NextResponse } from 'next/server'
import { getConfluenceCloudId } from '@/tools/confluence/utils'
export const dynamic = 'force-dynamic'
export async function POST(request: Request) {
try {
const { domain, accessToken, pageId, cloudId: providedCloudId } = await request.json()
if (!domain) {
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!pageId) {
return NextResponse.json({ error: 'Page ID is required' }, { status: 400 })
}
// Use provided cloudId or fetch it if not provided
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
// Build the URL for the Confluence API
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}?expand=body.storage,body.view,body.atlas_doc_format`
// Make the request to Confluence API
const response = await fetch(url, {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
console.error(`Confluence API error: ${response.status} ${response.statusText}`)
let errorMessage
try {
const errorData = await response.json()
console.error('Error details:', JSON.stringify(errorData, null, 2))
errorMessage = errorData.message || `Failed to fetch Confluence page (${response.status})`
} catch (e) {
console.error('Could not parse error response as JSON:', e)
errorMessage = `Failed to fetch Confluence page: ${response.status} ${response.statusText}`
}
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
// If body is empty, try to provide a minimal valid response
return NextResponse.json({
id: data.id,
title: data.title,
body: {
view: {
value:
data.body?.storage?.value ||
data.body?.view?.value ||
data.body?.atlas_doc_format?.value ||
data.content || // try alternative fields
data.description ||
`Content for page ${data.title}`, // fallback content
},
},
})
} catch (error) {
console.error('Error fetching Confluence page:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}
export async function PUT(request: Request) {
try {
const body = await request.json()
const {
domain,
accessToken,
pageId,
cloudId: providedCloudId,
title,
body: pageBody,
version,
} = body
if (!domain) {
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!pageId) {
return NextResponse.json({ error: 'Page ID is required' }, { status: 400 })
}
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
// First, get the current page to check its version
const currentPageUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}`
const currentPageResponse = await fetch(currentPageUrl, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${accessToken}`,
},
})
if (!currentPageResponse.ok) {
throw new Error(`Failed to fetch current page: ${currentPageResponse.status}`)
}
const currentPage = await currentPageResponse.json()
const currentVersion = currentPage.version.number
// Build the update body with incremented version
const updateBody: any = {
id: pageId,
version: {
number: currentVersion + 1,
message: version?.message || 'Updated via API',
},
title: title,
body: {
representation: 'storage',
value: pageBody?.value || '',
},
status: 'current',
}
const response = await fetch(currentPageUrl, {
method: 'PUT',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify(updateBody),
})
if (!response.ok) {
const errorData = await response.json().catch(() => null)
console.error('Confluence API error response:', {
status: response.status,
statusText: response.statusText,
error: JSON.stringify(errorData, null, 2),
})
const errorMessage =
errorData?.message ||
(errorData?.errors && JSON.stringify(errorData.errors)) ||
`Failed to update Confluence page (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Error updating Confluence page:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -1,108 +0,0 @@
import { NextResponse } from 'next/server'
import { getConfluenceCloudId } from '@/tools/confluence/utils'
export const dynamic = 'force-dynamic'
export async function POST(request: Request) {
try {
const {
domain,
accessToken,
title,
cloudId: providedCloudId,
limit = 50,
} = await request.json()
if (!domain) {
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
// Use provided cloudId or fetch it if not provided
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
// Build the URL with query parameters
const baseUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages`
const queryParams = new URLSearchParams()
if (limit) {
queryParams.append('limit', limit.toString())
}
if (title) {
queryParams.append('title', title)
}
const queryString = queryParams.toString()
const url = queryString ? `${baseUrl}?${queryString}` : baseUrl
console.log(`Fetching Confluence pages from: ${url}`)
// Make the request to Confluence API with OAuth Bearer token
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
})
console.log('Response status:', response.status, response.statusText)
if (!response.ok) {
console.error(`Confluence API error: ${response.status} ${response.statusText}`)
let errorMessage
try {
const errorData = await response.json()
console.error('Error details:', JSON.stringify(errorData, null, 2))
errorMessage = errorData.message || `Failed to fetch Confluence pages (${response.status})`
} catch (e) {
console.error('Could not parse error response as JSON:', e)
// Try to get the response text for more context
try {
const text = await response.text()
console.error('Response text:', text)
errorMessage = `Failed to fetch Confluence pages: ${response.status} ${response.statusText}`
} catch (_textError) {
errorMessage = `Failed to fetch Confluence pages: ${response.status} ${response.statusText}`
}
}
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
console.log('Confluence API response:', `${JSON.stringify(data, null, 2).substring(0, 300)}...`)
console.log(`Found ${data.results?.length || 0} pages`)
if (data.results && data.results.length > 0) {
console.log('First few pages:')
for (const page of data.results.slice(0, 3)) {
console.log(`- ${page.id}: ${page.title}`)
}
}
return NextResponse.json({
files: data.results.map((page: any) => ({
id: page.id,
name: page.title,
mimeType: 'confluence/page',
url: page._links?.webui || '',
modifiedTime: page.version?.createdAt || '',
spaceId: page.spaceId,
webViewLink: page._links?.webui || '',
})),
})
} catch (error) {
console.error('Error fetching Confluence pages:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,220 @@
/**
* Tests for OAuth connections API route
*
* @vitest-environment node
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createMockRequest } from '@/app/api/__test-utils__/utils'
describe('OAuth Connections API Route', () => {
const mockGetSession = vi.fn()
const mockDb = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn(),
}
const mockLogger = {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
}
const mockUUID = 'mock-uuid-12345678-90ab-cdef-1234-567890abcdef'
beforeEach(() => {
vi.resetModules()
vi.stubGlobal('crypto', {
randomUUID: vi.fn().mockReturnValue(mockUUID),
})
vi.doMock('@/lib/auth', () => ({
getSession: mockGetSession,
}))
vi.doMock('@/db', () => ({
db: mockDb,
}))
vi.doMock('@/db/schema', () => ({
account: { userId: 'userId', providerId: 'providerId' },
user: { email: 'email', id: 'id' },
}))
vi.doMock('drizzle-orm', () => ({
eq: vi.fn((field, value) => ({ field, value, type: 'eq' })),
}))
vi.doMock('jwt-decode', () => ({
jwtDecode: vi.fn(),
}))
vi.doMock('@/lib/logs/console-logger', () => ({
createLogger: vi.fn().mockReturnValue(mockLogger),
}))
})
afterEach(() => {
vi.clearAllMocks()
})
it('should return connections successfully', async () => {
mockGetSession.mockResolvedValueOnce({
user: { id: 'user-123' },
})
const mockAccounts = [
{
id: 'account-1',
providerId: 'google-email',
accountId: 'test@example.com',
scope: 'email profile',
updatedAt: new Date('2024-01-01'),
idToken: null,
},
{
id: 'account-2',
providerId: 'github',
accountId: 'testuser',
scope: 'repo',
updatedAt: new Date('2024-01-02'),
idToken: null,
},
]
const mockUserRecord = [{ email: 'user@example.com' }]
mockDb.select.mockReturnValueOnce(mockDb)
mockDb.from.mockReturnValueOnce(mockDb)
mockDb.where.mockResolvedValueOnce(mockAccounts)
mockDb.select.mockReturnValueOnce(mockDb)
mockDb.from.mockReturnValueOnce(mockDb)
mockDb.where.mockReturnValueOnce(mockDb)
mockDb.limit.mockResolvedValueOnce(mockUserRecord)
const req = createMockRequest('GET')
const { GET } = await import('./route')
const response = await GET(req)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.connections).toHaveLength(2)
expect(data.connections[0]).toMatchObject({
provider: 'google-email',
baseProvider: 'google',
featureType: 'email',
isConnected: true,
})
expect(data.connections[1]).toMatchObject({
provider: 'github',
baseProvider: 'github',
featureType: 'default',
isConnected: true,
})
})
it('should handle unauthenticated user', async () => {
mockGetSession.mockResolvedValueOnce(null)
const req = createMockRequest('GET')
const { GET } = await import('./route')
const response = await GET(req)
const data = await response.json()
expect(response.status).toBe(401)
expect(data.error).toBe('User not authenticated')
expect(mockLogger.warn).toHaveBeenCalled()
})
it('should handle user with no connections', async () => {
mockGetSession.mockResolvedValueOnce({
user: { id: 'user-123' },
})
mockDb.select.mockReturnValueOnce(mockDb)
mockDb.from.mockReturnValueOnce(mockDb)
mockDb.where.mockResolvedValueOnce([])
mockDb.select.mockReturnValueOnce(mockDb)
mockDb.from.mockReturnValueOnce(mockDb)
mockDb.where.mockReturnValueOnce(mockDb)
mockDb.limit.mockResolvedValueOnce([])
const req = createMockRequest('GET')
const { GET } = await import('./route')
const response = await GET(req)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.connections).toHaveLength(0)
})
it('should handle database error', async () => {
mockGetSession.mockResolvedValueOnce({
user: { id: 'user-123' },
})
mockDb.select.mockReturnValueOnce(mockDb)
mockDb.from.mockReturnValueOnce(mockDb)
mockDb.where.mockRejectedValueOnce(new Error('Database error'))
const req = createMockRequest('GET')
const { GET } = await import('./route')
const response = await GET(req)
const data = await response.json()
expect(response.status).toBe(500)
expect(data.error).toBe('Internal server error')
expect(mockLogger.error).toHaveBeenCalled()
})
it('should decode ID token for display name', async () => {
const { jwtDecode } = await import('jwt-decode')
const mockJwtDecode = jwtDecode as any
mockGetSession.mockResolvedValueOnce({
user: { id: 'user-123' },
})
const mockAccounts = [
{
id: 'account-1',
providerId: 'google',
accountId: 'google-user-id',
scope: 'email profile',
updatedAt: new Date('2024-01-01'),
idToken: 'mock-jwt-token',
},
]
mockJwtDecode.mockReturnValueOnce({
email: 'decoded@example.com',
name: 'Decoded User',
})
mockDb.select.mockReturnValueOnce(mockDb)
mockDb.from.mockReturnValueOnce(mockDb)
mockDb.where.mockResolvedValueOnce(mockAccounts)
mockDb.select.mockReturnValueOnce(mockDb)
mockDb.from.mockReturnValueOnce(mockDb)
mockDb.where.mockReturnValueOnce(mockDb)
mockDb.limit.mockResolvedValueOnce([])
const req = createMockRequest('GET')
const { GET } = await import('./route')
const response = await GET(req)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.connections[0].accounts[0].name).toBe('decoded@example.com')
})
})

View File

@@ -0,0 +1,256 @@
/**
* Tests for OAuth credentials API route
*
* @vitest-environment node
*/
import { NextRequest } from 'next/server'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
describe('OAuth Credentials API Route', () => {
const mockGetSession = vi.fn()
const mockParseProvider = vi.fn()
const mockDb = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn(),
}
const mockLogger = {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
}
const mockUUID = 'mock-uuid-12345678-90ab-cdef-1234-567890abcdef'
function createMockRequestWithQuery(method = 'GET', queryParams = ''): NextRequest {
const url = `http://localhost:3000/api/auth/oauth/credentials${queryParams}`
return new NextRequest(new URL(url), { method })
}
beforeEach(() => {
vi.resetModules()
vi.stubGlobal('crypto', {
randomUUID: vi.fn().mockReturnValue(mockUUID),
})
vi.doMock('@/lib/auth', () => ({
getSession: mockGetSession,
}))
vi.doMock('@/lib/oauth', () => ({
parseProvider: mockParseProvider,
}))
vi.doMock('@/db', () => ({
db: mockDb,
}))
vi.doMock('@/db/schema', () => ({
account: { userId: 'userId', providerId: 'providerId' },
user: { email: 'email', id: 'id' },
}))
vi.doMock('drizzle-orm', () => ({
and: vi.fn((...conditions) => ({ conditions, type: 'and' })),
eq: vi.fn((field, value) => ({ field, value, type: 'eq' })),
}))
vi.doMock('jwt-decode', () => ({
jwtDecode: vi.fn(),
}))
vi.doMock('@/lib/logs/console-logger', () => ({
createLogger: vi.fn().mockReturnValue(mockLogger),
}))
})
afterEach(() => {
vi.clearAllMocks()
})
it('should return credentials successfully', async () => {
mockGetSession.mockResolvedValueOnce({
user: { id: 'user-123' },
})
mockParseProvider.mockReturnValueOnce({
baseProvider: 'google',
})
const mockAccounts = [
{
id: 'credential-1',
userId: 'user-123',
providerId: 'google-email',
accountId: 'test@example.com',
updatedAt: new Date('2024-01-01'),
idToken: null,
},
{
id: 'credential-2',
userId: 'user-123',
providerId: 'google-default',
accountId: 'user-id',
updatedAt: new Date('2024-01-02'),
idToken: null,
},
]
mockDb.select.mockReturnValueOnce(mockDb)
mockDb.from.mockReturnValueOnce(mockDb)
mockDb.where.mockResolvedValueOnce(mockAccounts)
mockDb.select.mockReturnValueOnce(mockDb)
mockDb.from.mockReturnValueOnce(mockDb)
mockDb.where.mockReturnValueOnce(mockDb)
mockDb.limit.mockResolvedValueOnce([{ email: 'user@example.com' }])
const req = createMockRequestWithQuery('GET', '?provider=google-email')
const { GET } = await import('./route')
const response = await GET(req)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.credentials).toHaveLength(2)
expect(data.credentials[0]).toMatchObject({
id: 'credential-1',
provider: 'google-email',
isDefault: false,
})
expect(data.credentials[1]).toMatchObject({
id: 'credential-2',
provider: 'google-email',
isDefault: true,
})
})
it('should handle unauthenticated user', async () => {
mockGetSession.mockResolvedValueOnce(null)
const req = createMockRequestWithQuery('GET', '?provider=google')
const { GET } = await import('./route')
const response = await GET(req)
const data = await response.json()
expect(response.status).toBe(401)
expect(data.error).toBe('User not authenticated')
expect(mockLogger.warn).toHaveBeenCalled()
})
it('should handle missing provider parameter', async () => {
mockGetSession.mockResolvedValueOnce({
user: { id: 'user-123' },
})
const req = createMockRequestWithQuery('GET')
const { GET } = await import('./route')
const response = await GET(req)
const data = await response.json()
expect(response.status).toBe(400)
expect(data.error).toBe('Provider is required')
expect(mockLogger.warn).toHaveBeenCalled()
})
it('should handle no credentials found', async () => {
mockGetSession.mockResolvedValueOnce({
user: { id: 'user-123' },
})
mockParseProvider.mockReturnValueOnce({
baseProvider: 'github',
})
mockDb.select.mockReturnValueOnce(mockDb)
mockDb.from.mockReturnValueOnce(mockDb)
mockDb.where.mockResolvedValueOnce([])
const req = createMockRequestWithQuery('GET', '?provider=github')
const { GET } = await import('./route')
const response = await GET(req)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.credentials).toHaveLength(0)
})
it('should decode ID token for display name', async () => {
const { jwtDecode } = await import('jwt-decode')
const mockJwtDecode = jwtDecode as any
mockGetSession.mockResolvedValueOnce({
user: { id: 'user-123' },
})
mockParseProvider.mockReturnValueOnce({
baseProvider: 'google',
})
const mockAccounts = [
{
id: 'credential-1',
userId: 'user-123',
providerId: 'google-default',
accountId: 'google-user-id',
updatedAt: new Date('2024-01-01'),
idToken: 'mock-jwt-token',
},
]
mockJwtDecode.mockReturnValueOnce({
email: 'decoded@example.com',
name: 'Decoded User',
})
mockDb.select.mockReturnValueOnce(mockDb)
mockDb.from.mockReturnValueOnce(mockDb)
mockDb.where.mockResolvedValueOnce(mockAccounts)
const req = createMockRequestWithQuery('GET', '?provider=google')
const { GET } = await import('./route')
const response = await GET(req)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.credentials[0].name).toBe('decoded@example.com')
})
it('should handle database error', async () => {
mockGetSession.mockResolvedValueOnce({
user: { id: 'user-123' },
})
mockParseProvider.mockReturnValueOnce({
baseProvider: 'google',
})
mockDb.select.mockReturnValueOnce(mockDb)
mockDb.from.mockReturnValueOnce(mockDb)
mockDb.where.mockRejectedValueOnce(new Error('Database error'))
const req = createMockRequestWithQuery('GET', '?provider=google')
const { GET } = await import('./route')
const response = await GET(req)
const data = await response.json()
expect(response.status).toBe(500)
expect(data.error).toBe('Internal server error')
expect(mockLogger.error).toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,159 @@
/**
* Tests for OAuth disconnect API route
*
* @vitest-environment node
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createMockRequest } from '@/app/api/__test-utils__/utils'
describe('OAuth Disconnect API Route', () => {
const mockGetSession = vi.fn()
const mockDb = {
delete: vi.fn().mockReturnThis(),
where: vi.fn(),
}
const mockLogger = {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
}
const mockUUID = 'mock-uuid-12345678-90ab-cdef-1234-567890abcdef'
beforeEach(() => {
vi.resetModules()
vi.stubGlobal('crypto', {
randomUUID: vi.fn().mockReturnValue(mockUUID),
})
vi.doMock('@/lib/auth', () => ({
getSession: mockGetSession,
}))
vi.doMock('@/db', () => ({
db: mockDb,
}))
vi.doMock('@/db/schema', () => ({
account: { userId: 'userId', providerId: 'providerId' },
}))
vi.doMock('drizzle-orm', () => ({
and: vi.fn((...conditions) => ({ conditions, type: 'and' })),
eq: vi.fn((field, value) => ({ field, value, type: 'eq' })),
like: vi.fn((field, value) => ({ field, value, type: 'like' })),
or: vi.fn((...conditions) => ({ conditions, type: 'or' })),
}))
vi.doMock('@/lib/logs/console-logger', () => ({
createLogger: vi.fn().mockReturnValue(mockLogger),
}))
})
afterEach(() => {
vi.clearAllMocks()
})
it('should disconnect provider successfully', async () => {
mockGetSession.mockResolvedValueOnce({
user: { id: 'user-123' },
})
mockDb.delete.mockReturnValueOnce(mockDb)
mockDb.where.mockResolvedValueOnce(undefined)
const req = createMockRequest('POST', {
provider: 'google',
})
const { POST } = await import('./route')
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.success).toBe(true)
expect(mockLogger.info).toHaveBeenCalled()
})
it('should disconnect specific provider ID successfully', async () => {
mockGetSession.mockResolvedValueOnce({
user: { id: 'user-123' },
})
mockDb.delete.mockReturnValueOnce(mockDb)
mockDb.where.mockResolvedValueOnce(undefined)
const req = createMockRequest('POST', {
provider: 'google',
providerId: 'google-email',
})
const { POST } = await import('./route')
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.success).toBe(true)
expect(mockLogger.info).toHaveBeenCalled()
})
it('should handle unauthenticated user', async () => {
mockGetSession.mockResolvedValueOnce(null)
const req = createMockRequest('POST', {
provider: 'google',
})
const { POST } = await import('./route')
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(401)
expect(data.error).toBe('User not authenticated')
expect(mockLogger.warn).toHaveBeenCalled()
})
it('should handle missing provider', async () => {
mockGetSession.mockResolvedValueOnce({
user: { id: 'user-123' },
})
const req = createMockRequest('POST', {})
const { POST } = await import('./route')
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(400)
expect(data.error).toBe('Provider is required')
expect(mockLogger.warn).toHaveBeenCalled()
})
it('should handle database error', async () => {
mockGetSession.mockResolvedValueOnce({
user: { id: 'user-123' },
})
mockDb.delete.mockReturnValueOnce(mockDb)
mockDb.where.mockRejectedValueOnce(new Error('Database error'))
const req = createMockRequest('POST', {
provider: 'google',
})
const { POST } = await import('./route')
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(500)
expect(data.error).toBe('Internal server error')
expect(mockLogger.error).toHaveBeenCalled()
})
})

View File

@@ -1,138 +0,0 @@
import { NextResponse } from 'next/server'
import { createLogger } from '@/lib/logs/console-logger'
interface DiscordChannel {
id: string
name: string
type: number
guild_id?: string
}
export const dynamic = 'force-dynamic'
const logger = createLogger('DiscordChannelsAPI')
export async function POST(request: Request) {
try {
const { botToken, serverId, channelId } = await request.json()
if (!botToken) {
logger.error('Missing bot token in request')
return NextResponse.json({ error: 'Bot token is required' }, { status: 400 })
}
if (!serverId) {
logger.error('Missing server ID in request')
return NextResponse.json({ error: 'Server ID is required' }, { status: 400 })
}
// If channelId is provided, we'll fetch just that specific channel
if (channelId) {
logger.info(`Fetching single Discord channel: ${channelId}`)
// Fetch a specific channel by ID
const response = await fetch(`https://discord.com/api/v10/channels/${channelId}`, {
method: 'GET',
headers: {
Authorization: `Bot ${botToken}`,
'Content-Type': 'application/json',
},
})
if (!response.ok) {
logger.error('Discord API error fetching channel:', {
status: response.status,
statusText: response.statusText,
})
let errorMessage
try {
const errorData = await response.json()
logger.error('Error details:', errorData)
errorMessage = errorData.message || `Failed to fetch channel (${response.status})`
} catch (_e) {
errorMessage = `Failed to fetch channel: ${response.status} ${response.statusText}`
}
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const channel = (await response.json()) as DiscordChannel
// Verify this is a text channel and belongs to the requested server
if (channel.guild_id !== serverId) {
logger.error('Channel does not belong to the specified server')
return NextResponse.json(
{ error: 'Channel not found in specified server' },
{ status: 404 }
)
}
if (channel.type !== 0) {
logger.warn('Requested channel is not a text channel')
return NextResponse.json({ error: 'Channel is not a text channel' }, { status: 400 })
}
logger.info(`Successfully fetched channel: ${channel.name}`)
return NextResponse.json({
channel: {
id: channel.id,
name: channel.name,
type: channel.type,
},
})
}
logger.info(`Fetching all Discord channels for server: ${serverId}`)
// Fetch all channels from Discord API
const response = await fetch(`https://discord.com/api/v10/guilds/${serverId}/channels`, {
method: 'GET',
headers: {
Authorization: `Bot ${botToken}`,
'Content-Type': 'application/json',
},
})
if (!response.ok) {
logger.error('Discord API error:', {
status: response.status,
statusText: response.statusText,
})
let errorMessage
try {
const errorData = await response.json()
logger.error('Error details:', errorData)
errorMessage = errorData.message || `Failed to fetch channels (${response.status})`
} catch (_e) {
errorMessage = `Failed to fetch channels: ${response.status} ${response.statusText}`
}
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const channels = (await response.json()) as DiscordChannel[]
// Filter to just text channels (type 0)
const textChannels = channels.filter((channel: DiscordChannel) => channel.type === 0)
logger.info(`Successfully fetched ${textChannels.length} text channels`)
return NextResponse.json({
channels: textChannels.map((channel: DiscordChannel) => ({
id: channel.id,
name: channel.name,
type: channel.type,
})),
})
} catch (error) {
logger.error('Error processing request:', error)
return NextResponse.json(
{
error: 'Failed to retrieve Discord channels',
details: (error as Error).message,
},
{ status: 500 }
)
}
}

View File

@@ -1,117 +0,0 @@
import { NextResponse } from 'next/server'
import { createLogger } from '@/lib/logs/console-logger'
interface DiscordServer {
id: string
name: string
icon: string | null
}
export const dynamic = 'force-dynamic'
const logger = createLogger('DiscordServersAPI')
export async function POST(request: Request) {
try {
const { botToken, serverId } = await request.json()
if (!botToken) {
logger.error('Missing bot token in request')
return NextResponse.json({ error: 'Bot token is required' }, { status: 400 })
}
// If serverId is provided, we'll fetch just that server
if (serverId) {
logger.info(`Fetching single Discord server: ${serverId}`)
// Fetch a specific server by ID
const response = await fetch(`https://discord.com/api/v10/guilds/${serverId}`, {
method: 'GET',
headers: {
Authorization: `Bot ${botToken}`,
'Content-Type': 'application/json',
},
})
if (!response.ok) {
logger.error('Discord API error fetching server:', {
status: response.status,
statusText: response.statusText,
})
let errorMessage
try {
const errorData = await response.json()
logger.error('Error details:', errorData)
errorMessage = errorData.message || `Failed to fetch server (${response.status})`
} catch (_e) {
errorMessage = `Failed to fetch server: ${response.status} ${response.statusText}`
}
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const server = (await response.json()) as DiscordServer
logger.info(`Successfully fetched server: ${server.name}`)
return NextResponse.json({
server: {
id: server.id,
name: server.name,
icon: server.icon
? `https://cdn.discordapp.com/icons/${server.id}/${server.icon}.png`
: null,
},
})
}
// Otherwise, fetch all servers the bot is in
logger.info('Fetching all Discord servers')
const response = await fetch('https://discord.com/api/v10/users/@me/guilds', {
method: 'GET',
headers: {
Authorization: `Bot ${botToken}`,
'Content-Type': 'application/json',
},
})
if (!response.ok) {
logger.error('Discord API error:', {
status: response.status,
statusText: response.statusText,
})
let errorMessage
try {
const errorData = await response.json()
logger.error('Error details:', errorData)
errorMessage = errorData.message || `Failed to fetch servers (${response.status})`
} catch (_e) {
errorMessage = `Failed to fetch servers: ${response.status} ${response.statusText}`
}
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const servers = (await response.json()) as DiscordServer[]
logger.info(`Successfully fetched ${servers.length} servers`)
return NextResponse.json({
servers: servers.map((server: DiscordServer) => ({
id: server.id,
name: server.name,
icon: server.icon
? `https://cdn.discordapp.com/icons/${server.id}/${server.icon}.png`
: null,
})),
})
} catch (error) {
logger.error('Error processing request:', error)
return NextResponse.json(
{
error: 'Failed to retrieve Discord servers',
details: (error as Error).message,
},
{ status: 500 }
)
}
}

View File

@@ -1,123 +0,0 @@
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { db } from '@/db'
import { account } from '@/db/schema'
import { refreshAccessTokenIfNeeded } from '../../utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('GoogleDriveFileAPI')
/**
* Get a single file from Google Drive
*/
export async function GET(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8) // Generate a short request ID for correlation
logger.info(`[${requestId}] Google Drive file request received`)
try {
// Get the session
const session = await getSession()
// Check if the user is authenticated
if (!session?.user?.id) {
logger.warn(`[${requestId}] Unauthenticated request rejected`)
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
}
// Get the credential ID and file ID from the query params
const { searchParams } = new URL(request.url)
const credentialId = searchParams.get('credentialId')
const fileId = searchParams.get('fileId')
if (!credentialId || !fileId) {
logger.warn(`[${requestId}] Missing required parameters`)
return NextResponse.json({ error: 'Credential ID and File ID are required' }, { status: 400 })
}
// Get the credential from the database
const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
if (!credentials.length) {
logger.warn(`[${requestId}] Credential not found`, { credentialId })
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
}
const credential = credentials[0]
// Check if the credential belongs to the user
if (credential.userId !== session.user.id) {
logger.warn(`[${requestId}] Unauthorized credential access attempt`, {
credentialUserId: credential.userId,
requestUserId: session.user.id,
})
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
}
// Refresh access token if needed using the utility function
const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId)
if (!accessToken) {
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
}
// Fetch the file from Google Drive API
logger.info(`[${requestId}] Fetching file ${fileId} from Google Drive API`)
const response = await fetch(
`https://www.googleapis.com/drive/v3/files/${fileId}?fields=id,name,mimeType,iconLink,webViewLink,thumbnailLink,createdTime,modifiedTime,size,owners,exportLinks`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
)
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: { message: 'Unknown error' } }))
logger.error(`[${requestId}] Google Drive API error`, {
status: response.status,
error: errorData.error?.message || 'Failed to fetch file from Google Drive',
})
return NextResponse.json(
{
error: errorData.error?.message || 'Failed to fetch file from Google Drive',
},
{ status: response.status }
)
}
const file = await response.json()
// In case of Google Docs, Sheets, etc., provide the export links
const exportFormats: { [key: string]: string } = {
'application/vnd.google-apps.document': 'application/pdf', // Google Docs to PDF
'application/vnd.google-apps.spreadsheet':
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // Google Sheets to XLSX
'application/vnd.google-apps.presentation': 'application/pdf', // Google Slides to PDF
}
// If the file is a Google Docs, Sheets, or Slides file, we need to provide the export link
if (file.mimeType.startsWith('application/vnd.google-apps.')) {
const format = exportFormats[file.mimeType] || 'application/pdf'
if (!file.exportLinks) {
// If export links are not available in the response, try to construct one
file.downloadUrl = `https://www.googleapis.com/drive/v3/files/${file.id}/export?mimeType=${encodeURIComponent(
format
)}`
} else {
// Use the export link from the response if available
file.downloadUrl = file.exportLinks[format]
}
} else {
// For regular files, use the download link
file.downloadUrl = `https://www.googleapis.com/drive/v3/files/${file.id}?alt=media`
}
return NextResponse.json({ file }, { status: 200 })
} catch (error) {
logger.error(`[${requestId}] Error fetching file from Google Drive`, error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}

View File

@@ -1,130 +0,0 @@
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { db } from '@/db'
import { account } from '@/db/schema'
import { refreshAccessTokenIfNeeded } from '../../utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('GoogleDriveFilesAPI')
/**
* Get files from Google Drive
*/
export async function GET(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8) // Generate a short request ID for correlation
logger.info(`[${requestId}] Google Drive files request received`)
try {
// Get the session
const session = await getSession()
// Check if the user is authenticated
if (!session?.user?.id) {
logger.warn(`[${requestId}] Unauthenticated request rejected`)
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
}
// Get the credential ID from the query params
const { searchParams } = new URL(request.url)
const credentialId = searchParams.get('credentialId')
const mimeType = searchParams.get('mimeType')
const query = searchParams.get('query') || ''
if (!credentialId) {
logger.warn(`[${requestId}] Missing credential ID`)
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
}
// Get the credential from the database
const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
if (!credentials.length) {
logger.warn(`[${requestId}] Credential not found`, { credentialId })
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
}
const credential = credentials[0]
// Check if the credential belongs to the user
if (credential.userId !== session.user.id) {
logger.warn(`[${requestId}] Unauthorized credential access attempt`, {
credentialUserId: credential.userId,
requestUserId: session.user.id,
})
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
}
// Refresh access token if needed using the utility function
const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId)
if (!accessToken) {
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
}
// Build the query parameters for Google Drive API
let queryParams = 'trashed=false'
// Add mimeType filter if provided
if (mimeType) {
// For Google Drive API, we need to use 'q' parameter for mimeType filtering
// Instead of using the mimeType parameter directly, we'll add it to the query
if (queryParams.includes('q=')) {
queryParams += ` and mimeType='${mimeType}'`
} else {
queryParams += `&q=mimeType='${mimeType}'`
}
}
// Add search query if provided
if (query) {
if (queryParams.includes('q=')) {
queryParams += ` and name contains '${query}'`
} else {
queryParams += `&q=name contains '${query}'`
}
}
// Fetch files from Google Drive API
const response = await fetch(
`https://www.googleapis.com/drive/v3/files?${queryParams}&fields=files(id,name,mimeType,iconLink,webViewLink,thumbnailLink,createdTime,modifiedTime,size,owners)`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
)
if (!response.ok) {
const error = await response.json().catch(() => ({ error: { message: 'Unknown error' } }))
logger.error(`[${requestId}] Google Drive API error`, {
status: response.status,
error: error.error?.message || 'Failed to fetch files from Google Drive',
})
return NextResponse.json(
{
error: error.error?.message || 'Failed to fetch files from Google Drive',
},
{ status: response.status }
)
}
const data = await response.json()
let files = data.files || []
if (mimeType === 'application/vnd.google-apps.spreadsheet') {
files = files.filter(
(file: any) => file.mimeType === 'application/vnd.google-apps.spreadsheet'
)
} else if (mimeType === 'application/vnd.google-apps.document') {
files = files.filter((file: any) => file.mimeType === 'application/vnd.google-apps.document')
}
return NextResponse.json({ files }, { status: 200 })
} catch (error) {
logger.error(`[${requestId}] Error fetching files from Google Drive`, error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}

View File

@@ -1,115 +0,0 @@
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { db } from '@/db'
import { account } from '@/db/schema'
import { refreshAccessTokenIfNeeded } from '../../utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('GmailLabelAPI')
export async function GET(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
try {
// Get the session
const session = await getSession()
// Check if the user is authenticated
if (!session?.user?.id) {
logger.warn(`[${requestId}] Unauthenticated label request rejected`)
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
}
const { searchParams } = new URL(request.url)
const credentialId = searchParams.get('credentialId')
const labelId = searchParams.get('labelId')
if (!credentialId || !labelId) {
logger.warn(`[${requestId}] Missing required parameters`)
return NextResponse.json(
{ error: 'Credential ID and Label ID are required' },
{ status: 400 }
)
}
// Get the credential from the database
const credentials = await db
.select()
.from(account)
.where(and(eq(account.id, credentialId), eq(account.userId, session.user.id)))
.limit(1)
if (!credentials.length) {
logger.warn(`[${requestId}] Credential not found`)
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
}
const credential = credentials[0]
// Log the credential info (without exposing sensitive data)
logger.info(
`[${requestId}] Using credential: ${credential.id}, provider: ${credential.providerId}`
)
// Refresh access token if needed using the utility function
const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId)
if (!accessToken) {
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
}
// Fetch specific label from Gmail API
logger.info(`[${requestId}] Fetching label ${labelId} from Gmail API`)
const response = await fetch(
`https://gmail.googleapis.com/gmail/v1/users/me/labels/${labelId}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
)
// Log the response status
logger.info(`[${requestId}] Gmail API response status: ${response.status}`)
if (!response.ok) {
const errorText = await response.text()
logger.error(`[${requestId}] Gmail API error response: ${errorText}`)
try {
const error = JSON.parse(errorText)
return NextResponse.json({ error }, { status: response.status })
} catch (_e) {
return NextResponse.json({ error: errorText }, { status: response.status })
}
}
const label = await response.json()
// Transform the label to a more usable format
// Format the label name with proper capitalization
let formattedName = label.name
// Handle system labels (INBOX, SENT, etc.)
if (label.type === 'system') {
// Convert to title case (first letter uppercase, rest lowercase)
formattedName = label.name.charAt(0).toUpperCase() + label.name.slice(1).toLowerCase()
}
const formattedLabel = {
id: label.id,
name: formattedName,
type: label.type,
messagesTotal: label.messagesTotal || 0,
messagesUnread: label.messagesUnread || 0,
}
return NextResponse.json({ label: formattedLabel }, { status: 200 })
} catch (error) {
logger.error(`[${requestId}] Error fetching Gmail label:`, error)
return NextResponse.json({ error: 'Failed to fetch Gmail label' }, { status: 500 })
}
}

View File

@@ -1,129 +0,0 @@
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { db } from '@/db'
import { account } from '@/db/schema'
import { refreshAccessTokenIfNeeded } from '../../utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('GmailLabelsAPI')
interface GmailLabel {
id: string
name: string
type: 'system' | 'user'
messagesTotal?: number
messagesUnread?: number
}
export async function GET(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
try {
// Get the session
const session = await getSession()
// Check if the user is authenticated
if (!session?.user?.id) {
logger.warn(`[${requestId}] Unauthenticated labels request rejected`)
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
}
const { searchParams } = new URL(request.url)
const credentialId = searchParams.get('credentialId')
const query = searchParams.get('query')
if (!credentialId) {
logger.warn(`[${requestId}] Missing credentialId parameter`)
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
}
// Get the credential from the database
const credentials = await db
.select()
.from(account)
.where(and(eq(account.id, credentialId), eq(account.userId, session.user.id)))
.limit(1)
if (!credentials.length) {
logger.warn(`[${requestId}] Credential not found`)
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
}
const credential = credentials[0]
// Log the credential info (without exposing sensitive data)
logger.info(
`[${requestId}] Using credential: ${credential.id}, provider: ${credential.providerId}`
)
// Refresh access token if needed using the utility function
const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId)
if (!accessToken) {
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
}
// Fetch labels from Gmail API
const response = await fetch('https://gmail.googleapis.com/gmail/v1/users/me/labels', {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
// Log the response status
logger.info(`[${requestId}] Gmail API response status: ${response.status}`)
if (!response.ok) {
const errorText = await response.text()
logger.error(`[${requestId}] Gmail API error response: ${errorText}`)
try {
const error = JSON.parse(errorText)
return NextResponse.json({ error }, { status: response.status })
} catch (_e) {
return NextResponse.json({ error: errorText }, { status: response.status })
}
}
const data = await response.json()
if (!Array.isArray(data.labels)) {
logger.error(`[${requestId}] Unexpected labels response structure:`, data)
return NextResponse.json({ error: 'Invalid labels response' }, { status: 500 })
}
// Transform the labels to a more usable format
const labels = data.labels.map((label: GmailLabel) => {
// Format the label name with proper capitalization
let formattedName = label.name
// Handle system labels (INBOX, SENT, etc.)
if (label.type === 'system') {
// Convert to title case (first letter uppercase, rest lowercase)
formattedName = label.name.charAt(0).toUpperCase() + label.name.slice(1).toLowerCase()
}
return {
id: label.id,
name: formattedName,
type: label.type,
messagesTotal: label.messagesTotal || 0,
messagesUnread: label.messagesUnread || 0,
}
})
// Filter labels if a query is provided
const filteredLabels = query
? labels.filter((label: GmailLabel) =>
label.name.toLowerCase().includes((query as string).toLowerCase())
)
: labels
return NextResponse.json({ labels: filteredLabels }, { status: 200 })
} catch (error) {
logger.error(`[${requestId}] Error fetching Gmail labels:`, error)
return NextResponse.json({ error: 'Failed to fetch Gmail labels' }, { status: 500 })
}
}

View File

@@ -1,101 +0,0 @@
import { NextResponse } from 'next/server'
import { Logger } from '@/lib/logs/console-logger'
import { getJiraCloudId } from '@/tools/jira/utils'
export const dynamic = 'force-dynamic'
const logger = new Logger('jira_issue')
export async function POST(request: Request) {
try {
const { domain, accessToken, issueId, cloudId: providedCloudId } = await request.json()
// Add detailed request logging
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 (!issueId) {
logger.error('Missing issue ID in request')
return NextResponse.json({ error: 'Issue ID is required' }, { status: 400 })
}
// Use provided cloudId or fetch it if not provided
const cloudId = providedCloudId || (await getJiraCloudId(domain, accessToken))
logger.info('Using cloud ID:', cloudId)
// Build the URL using cloudId for Jira API
const url = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${issueId}`
logger.info('Fetching Jira issue from:', url)
// Make the request to Jira API
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
})
if (!response.ok) {
logger.error('Jira API error:', {
status: response.status,
statusText: response.statusText,
})
let errorMessage
try {
const errorData = await response.json()
logger.error('Error details:', errorData)
errorMessage = errorData.message || `Failed to fetch issue (${response.status})`
} catch (_e) {
errorMessage = `Failed to fetch issue: ${response.status} ${response.statusText}`
}
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
logger.info('Successfully fetched issue:', data.key)
// Transform the Jira issue data into our expected format
const issueInfo: any = {
id: data.key,
name: data.fields.summary,
mimeType: 'jira/issue',
url: `https://${domain}/browse/${data.key}`,
modifiedTime: data.fields.updated,
webViewLink: `https://${domain}/browse/${data.key}`,
// Add additional fields that might be needed for the workflow
status: data.fields.status?.name,
description: data.fields.description,
priority: data.fields.priority?.name,
assignee: data.fields.assignee?.displayName,
reporter: data.fields.reporter?.displayName,
project: {
key: data.fields.project?.key,
name: data.fields.project?.name,
},
}
return NextResponse.json({
issue: issueInfo,
cloudId, // Return the cloudId so it can be cached
})
} catch (error) {
logger.error('Error processing request:', error)
// Add more context to the error response
return NextResponse.json(
{
error: 'Failed to retrieve Jira issue',
details: (error as Error).message,
},
{ status: 500 }
)
}
}

View File

@@ -1,175 +0,0 @@
import { NextResponse } from 'next/server'
import { Logger } from '@/lib/logs/console-logger'
import { getJiraCloudId } from '@/tools/jira/utils'
export const dynamic = 'force-dynamic'
const logger = new Logger('jira_issues')
export async function POST(request: Request) {
try {
const { domain, accessToken, issueKeys = [], cloudId: providedCloudId } = await request.json()
if (!domain) {
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (issueKeys.length === 0) {
logger.info('No issue keys provided, returning empty result')
return NextResponse.json({ issues: [] })
}
// Use provided cloudId or fetch it if not provided
const cloudId = providedCloudId || (await getJiraCloudId(domain, accessToken))
// Build the URL using cloudId for Jira API
const url = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/bulkfetch`
// Prepare the request body for bulk fetch
const requestBody = {
expand: ['names'],
fields: ['summary', 'status', 'assignee', 'updated', 'project'],
fieldsByKeys: false,
issueIdsOrKeys: issueKeys,
properties: [],
}
// Make the request to Jira API with OAuth Bearer token
const requestConfig = {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
}
const response = await fetch(url, requestConfig)
if (!response.ok) {
logger.error(`Jira API error: ${response.status} ${response.statusText}`)
let errorMessage
try {
const errorData = await response.json()
logger.error('Error details:', JSON.stringify(errorData, null, 2))
errorMessage = errorData.message || `Failed to fetch Jira issues (${response.status})`
} catch (e) {
logger.error('Could not parse error response as JSON:', e)
try {
const _text = await response.text()
errorMessage = `Failed to fetch Jira issues: ${response.status} ${response.statusText}`
} catch (_textError) {
errorMessage = `Failed to fetch Jira issues: ${response.status} ${response.statusText}`
}
}
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
if (data.issues && data.issues.length > 0) {
data.issues.slice(0, 3).forEach((issue: any) => {
logger.info(`- ${issue.key}: ${issue.fields.summary}`)
})
}
return NextResponse.json({
issues: data.issues
? data.issues.map((issue: any) => ({
id: issue.key,
name: issue.fields.summary,
mimeType: 'jira/issue',
url: `https://${domain}/browse/${issue.key}`,
modifiedTime: issue.fields.updated,
webViewLink: `https://${domain}/browse/${issue.key}`,
}))
: [],
cloudId, // Return the cloudId so it can be cached
})
} catch (error) {
logger.error('Error fetching Jira issues:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}
export async function GET(request: Request) {
try {
const url = new URL(request.url)
const domain = url.searchParams.get('domain')?.trim()
const accessToken = url.searchParams.get('accessToken')
const providedCloudId = url.searchParams.get('cloudId')
const query = url.searchParams.get('query') || ''
if (!domain) {
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
// Use provided cloudId or fetch it if not provided
const cloudId = providedCloudId || (await getJiraCloudId(domain, accessToken))
logger.info('Using cloud ID:', cloudId)
// Build query parameters
const params = new URLSearchParams()
// Only add query if it exists
if (query) {
params.append('query', query)
}
// Use the correct Jira Cloud OAuth endpoint structure
const apiUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/picker?${params.toString()}`
logger.info(`Fetching Jira issue suggestions from: ${apiUrl}`)
const response = await fetch(apiUrl, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
})
logger.info('Response status:', response.status, response.statusText)
if (!response.ok) {
logger.error(`Jira API error: ${response.status} ${response.statusText}`)
let errorMessage
try {
const errorData = await response.json()
logger.error('Error details:', errorData)
errorMessage = errorData.message || `Failed to fetch issue suggestions (${response.status})`
} catch (_e) {
errorMessage = `Failed to fetch issue suggestions: ${response.status} ${response.statusText}`
}
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
return NextResponse.json({
...data,
cloudId, // Return the cloudId so it can be cached
})
} catch (error) {
logger.error('Error fetching Jira issue suggestions:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -1,164 +0,0 @@
import { NextResponse } from 'next/server'
import { Logger } from '@/lib/logs/console-logger'
import { getJiraCloudId } from '@/tools/jira/utils'
export const dynamic = 'force-dynamic'
const logger = new Logger('jira_projects')
export async function GET(request: Request) {
try {
const url = new URL(request.url)
const domain = url.searchParams.get('domain')?.trim()
const accessToken = url.searchParams.get('accessToken')
const providedCloudId = url.searchParams.get('cloudId')
const query = url.searchParams.get('query') || ''
if (!domain) {
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
// Use provided cloudId or fetch it if not provided
const cloudId = providedCloudId || (await getJiraCloudId(domain, accessToken))
logger.info(`Using cloud ID: ${cloudId}`)
// Build the URL for the Jira API projects endpoint
const apiUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/project/search`
// Add query parameters if searching
const queryParams = new URLSearchParams()
if (query) {
queryParams.append('query', query)
}
// Add other useful parameters
queryParams.append('orderBy', 'name')
queryParams.append('expand', 'description,lead,url,projectKeys')
const finalUrl = `${apiUrl}?${queryParams.toString()}`
logger.info(`Fetching Jira projects from: ${finalUrl}`)
const response = await fetch(finalUrl, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
})
logger.info(`Response status: ${response.status} ${response.statusText}`)
if (!response.ok) {
logger.error(`Jira API error: ${response.status} ${response.statusText}`)
let errorMessage
try {
const errorData = await response.json()
logger.error('Error details:', errorData)
errorMessage = errorData.message || `Failed to fetch projects (${response.status})`
} catch (_e) {
errorMessage = `Failed to fetch projects: ${response.status} ${response.statusText}`
}
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
// Add detailed logging
logger.info(`Jira API Response Status: ${response.status}`)
logger.info(`Found projects: ${data.values?.length || 0}`)
// Transform the response to match our expected format
const projects =
data.values?.map((project: any) => ({
id: project.id,
key: project.key,
name: project.name,
url: project.self,
avatarUrl: project.avatarUrls?.['48x48'], // Use the medium size avatar
description: project.description,
projectTypeKey: project.projectTypeKey,
simplified: project.simplified,
style: project.style,
isPrivate: project.isPrivate,
})) || []
return NextResponse.json({
projects,
cloudId, // Return the cloudId so it can be cached
})
} catch (error) {
logger.error('Error fetching Jira projects:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}
// For individual project retrieval if needed
export async function POST(request: Request) {
try {
const { domain, accessToken, projectId, cloudId: providedCloudId } = await request.json()
if (!domain) {
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!projectId) {
return NextResponse.json({ error: 'Project ID is required' }, { status: 400 })
}
// Use provided cloudId or fetch it if not provided
const cloudId = providedCloudId || (await getJiraCloudId(domain, accessToken))
const apiUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/project/${projectId}`
const response = await fetch(apiUrl, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
})
if (!response.ok) {
const errorData = await response.json()
logger.error('Error details:', errorData)
return NextResponse.json(
{ error: errorData.message || `Failed to fetch project (${response.status})` },
{ status: response.status }
)
}
const project = await response.json()
return NextResponse.json({
project: {
id: project.id,
key: project.key,
name: project.name,
url: project.self,
avatarUrl: project.avatarUrls?.['48x48'],
description: project.description,
projectTypeKey: project.projectTypeKey,
simplified: project.simplified,
style: project.style,
isPrivate: project.isPrivate,
},
cloudId,
})
} catch (error) {
logger.error('Error fetching Jira project:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -1,121 +0,0 @@
import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { refreshAccessTokenIfNeeded } from '../../utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('TeamsChannelsAPI')
export async function POST(request: Request) {
try {
const session = await getSession()
const body = await request.json()
const { credential, teamId, workflowId } = body
if (!credential) {
logger.error('Missing credential in request')
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}
if (!teamId) {
logger.error('Missing team ID in request')
return NextResponse.json({ error: 'Team ID is required' }, { status: 400 })
}
try {
// Get the userId either from the session or from the workflowId
const userId = session?.user?.id || ''
if (!userId) {
logger.error('No user ID found in session')
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
}
const accessToken = await refreshAccessTokenIfNeeded(credential, userId, workflowId)
if (!accessToken) {
logger.error('Failed to get access token', { credentialId: credential, userId })
return NextResponse.json(
{
error: 'Could not retrieve access token',
authRequired: true,
},
{ status: 401 }
)
}
const response = await fetch(
`https://graph.microsoft.com/v1.0/teams/${encodeURIComponent(teamId)}/channels`,
{
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
}
)
if (!response.ok) {
const errorData = await response.json()
logger.error('Microsoft Graph API error getting channels', {
status: response.status,
error: errorData,
endpoint: `https://graph.microsoft.com/v1.0/teams/${teamId}/channels`,
})
// Check for auth errors specifically
if (response.status === 401) {
return NextResponse.json(
{
error: 'Authentication failed. Please reconnect your Microsoft Teams account.',
authRequired: true,
},
{ status: 401 }
)
}
throw new Error(`Microsoft Graph API error: ${JSON.stringify(errorData)}`)
}
const data = await response.json()
const channels = data.value
return NextResponse.json({
channels: channels,
})
} catch (innerError) {
logger.error('Error during API requests:', innerError)
// Check if it's an authentication error
const errorMessage = innerError instanceof Error ? innerError.message : String(innerError)
if (
errorMessage.includes('auth') ||
errorMessage.includes('token') ||
errorMessage.includes('unauthorized') ||
errorMessage.includes('unauthenticated')
) {
return NextResponse.json(
{
error: 'Authentication failed. Please reconnect your Microsoft Teams account.',
authRequired: true,
details: errorMessage,
},
{ status: 401 }
)
}
throw innerError
}
} catch (error) {
logger.error('Error processing Channels request:', error)
return NextResponse.json(
{
error: 'Failed to retrieve Microsoft Teams channels',
details: (error as Error).message,
},
{ status: 500 }
)
}
}

View File

@@ -1,221 +0,0 @@
import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { refreshAccessTokenIfNeeded } from '../../utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('teams-chats')
// Helper function to get chat members and create a meaningful name
const getChatDisplayName = async (
chatId: string,
accessToken: string,
chatTopic?: string
): Promise<string> => {
try {
// If the chat already has a topic, use it
if (chatTopic?.trim() && chatTopic !== 'null') {
return chatTopic
}
// Fetch chat members to create a meaningful name
const membersResponse = await fetch(
`https://graph.microsoft.com/v1.0/chats/${chatId}/members`,
{
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
}
)
if (membersResponse.ok) {
const membersData = await membersResponse.json()
const members = membersData.value || []
// Filter out the current user and get display names
const memberNames = members
.filter((member: any) => member.displayName && member.displayName !== 'Unknown')
.map((member: any) => member.displayName)
.slice(0, 3) // Limit to first 3 names to avoid very long names
if (memberNames.length > 0) {
if (memberNames.length === 1) {
return memberNames[0] // 1:1 chat
}
if (memberNames.length === 2) {
return memberNames.join(' & ') // 2-person group
}
return `${memberNames.slice(0, 2).join(', ')} & ${memberNames.length - 2} more` // Larger group
}
}
// Fallback: try to get a better name from recent messages
try {
const messagesResponse = await fetch(
`https://graph.microsoft.com/v1.0/chats/${chatId}/messages?$top=10&$orderby=createdDateTime desc`,
{
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
}
)
if (messagesResponse.ok) {
const messagesData = await messagesResponse.json()
const messages = messagesData.value || []
// Look for chat rename events
for (const message of messages) {
if (message.eventDetail?.chatDisplayName) {
return message.eventDetail.chatDisplayName
}
}
// Get unique sender names from recent messages as last resort
const senderNames = [
...new Set(
messages
.filter(
(msg: any) => msg.from?.user?.displayName && msg.from.user.displayName !== 'Unknown'
)
.map((msg: any) => msg.from.user.displayName)
),
].slice(0, 3)
if (senderNames.length > 0) {
if (senderNames.length === 1) {
return senderNames[0] as string
}
if (senderNames.length === 2) {
return senderNames.join(' & ')
}
return `${senderNames.slice(0, 2).join(', ')} & ${senderNames.length - 2} more`
}
}
} catch (error) {
logger.warn(
`Failed to get better name from messages for chat ${chatId}: ${error instanceof Error ? error.message : String(error)}`
)
}
// Final fallback
return `Chat ${chatId.split(':')[0] || chatId.substring(0, 8)}...`
} catch (error) {
logger.warn(
`Failed to get display name for chat ${chatId}: ${error instanceof Error ? error.message : String(error)}`
)
return `Chat ${chatId.split(':')[0] || chatId.substring(0, 8)}...`
}
}
export async function POST(request: Request) {
try {
const session = await getSession()
const body = await request.json()
const { credential } = body
if (!credential) {
logger.error('Missing credential in request')
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}
try {
// Get the userId either from the session or from the workflowId
const userId = session?.user?.id || ''
if (!userId) {
logger.error('No user ID found in session')
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
}
const accessToken = await refreshAccessTokenIfNeeded(credential, userId, body.workflowId)
if (!accessToken) {
logger.error('Failed to get access token', { credentialId: credential, userId })
return NextResponse.json({ error: 'Could not retrieve access token' }, { status: 401 })
}
// Now try to fetch the chats
const response = await fetch('https://graph.microsoft.com/v1.0/me/chats', {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
})
if (!response.ok) {
const errorData = await response.json()
logger.error('Microsoft Graph API error getting chats', {
status: response.status,
error: errorData,
endpoint: 'https://graph.microsoft.com/v1.0/me/chats',
})
// Check for auth errors specifically
if (response.status === 401) {
return NextResponse.json(
{
error: 'Authentication failed. Please reconnect your Microsoft Teams account.',
authRequired: true,
},
{ status: 401 }
)
}
throw new Error(`Microsoft Graph API error: ${JSON.stringify(errorData)}`)
}
const data = await response.json()
// Process chats with enhanced display names
const chats = await Promise.all(
data.value.map(async (chat: any) => ({
id: chat.id,
displayName: await getChatDisplayName(chat.id, accessToken, chat.topic),
}))
)
return NextResponse.json({
chats: chats,
})
} catch (innerError) {
logger.error('Error during API requests:', innerError)
// Check if it's an authentication error
const errorMessage = innerError instanceof Error ? innerError.message : String(innerError)
if (
errorMessage.includes('auth') ||
errorMessage.includes('token') ||
errorMessage.includes('unauthorized') ||
errorMessage.includes('unauthenticated')
) {
return NextResponse.json(
{
error: 'Authentication failed. Please reconnect your Microsoft Teams account.',
authRequired: true,
details: errorMessage,
},
{ status: 401 }
)
}
throw innerError
}
} catch (error) {
logger.error('Error processing Chats request:', error)
return NextResponse.json(
{
error: 'Failed to retrieve Microsoft Teams chats',
details: (error as Error).message,
},
{ status: 500 }
)
}
}

View File

@@ -1,113 +0,0 @@
import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { refreshAccessTokenIfNeeded } from '../../utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('teams-teams')
export async function POST(request: Request) {
try {
const session = await getSession()
const body = await request.json()
const { credential, workflowId } = body
if (!credential) {
logger.error('Missing credential in request')
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}
try {
// Get the userId either from the session or from the workflowId
const userId = session?.user?.id || ''
if (!userId) {
logger.error('No user ID found in session')
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
}
const accessToken = await refreshAccessTokenIfNeeded(credential, userId, workflowId)
if (!accessToken) {
logger.error('Failed to get access token', { credentialId: credential, userId })
return NextResponse.json(
{
error: 'Could not retrieve access token',
authRequired: true,
},
{ status: 401 }
)
}
const response = await fetch('https://graph.microsoft.com/v1.0/me/joinedTeams', {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
})
if (!response.ok) {
const errorData = await response.json()
logger.error('Microsoft Graph API error getting teams', {
status: response.status,
error: errorData,
endpoint: 'https://graph.microsoft.com/v1.0/me/joinedTeams',
})
// Check for auth errors specifically
if (response.status === 401) {
return NextResponse.json(
{
error: 'Authentication failed. Please reconnect your Microsoft Teams account.',
authRequired: true,
},
{ status: 401 }
)
}
throw new Error(`Microsoft Graph API error: ${JSON.stringify(errorData)}`)
}
const data = await response.json()
const teams = data.value
return NextResponse.json({
teams: teams,
})
} catch (innerError) {
logger.error('Error during API requests:', innerError)
// Check if it's an authentication error
const errorMessage = innerError instanceof Error ? innerError.message : String(innerError)
if (
errorMessage.includes('auth') ||
errorMessage.includes('token') ||
errorMessage.includes('unauthorized') ||
errorMessage.includes('unauthenticated')
) {
return NextResponse.json(
{
error: 'Authentication failed. Please reconnect your Microsoft Teams account.',
authRequired: true,
details: errorMessage,
},
{ status: 401 }
)
}
throw innerError
}
} catch (error) {
logger.error('Error processing Teams request:', error)
return NextResponse.json(
{
error: 'Failed to retrieve Microsoft Teams teams',
details: (error as Error).message,
},
{ status: 500 }
)
}
}

View File

@@ -1,132 +0,0 @@
import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { refreshAccessTokenIfNeeded } from '../../utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('OutlookFoldersAPI')
interface OutlookFolder {
id: string
displayName: string
totalItemCount?: number
unreadItemCount?: number
}
export async function GET(request: Request) {
try {
const session = await getSession()
const { searchParams } = new URL(request.url)
const credentialId = searchParams.get('credentialId')
if (!credentialId) {
logger.error('Missing credentialId in request')
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
}
try {
// Get the userId from the session
const userId = session?.user?.id || ''
if (!userId) {
logger.error('No user ID found in session')
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
}
const accessToken = await refreshAccessTokenIfNeeded(
credentialId,
userId,
crypto.randomUUID().slice(0, 8)
)
if (!accessToken) {
logger.error('Failed to get access token', { credentialId, userId })
return NextResponse.json(
{
error: 'Could not retrieve access token',
authRequired: true,
},
{ status: 401 }
)
}
const response = await fetch('https://graph.microsoft.com/v1.0/me/mailFolders', {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
})
if (!response.ok) {
const errorData = await response.json()
logger.error('Microsoft Graph API error getting folders', {
status: response.status,
error: errorData,
endpoint: 'https://graph.microsoft.com/v1.0/me/mailFolders',
})
// Check for auth errors specifically
if (response.status === 401) {
return NextResponse.json(
{
error: 'Authentication failed. Please reconnect your Outlook account.',
authRequired: true,
},
{ status: 401 }
)
}
throw new Error(`Microsoft Graph API error: ${JSON.stringify(errorData)}`)
}
const data = await response.json()
const folders = data.value || []
// Transform folders to match the expected format
const transformedFolders = folders.map((folder: OutlookFolder) => ({
id: folder.id,
name: folder.displayName,
type: 'folder',
messagesTotal: folder.totalItemCount || 0,
messagesUnread: folder.unreadItemCount || 0,
}))
return NextResponse.json({
folders: transformedFolders,
})
} catch (innerError) {
logger.error('Error during API requests:', innerError)
// Check if it's an authentication error
const errorMessage = innerError instanceof Error ? innerError.message : String(innerError)
if (
errorMessage.includes('auth') ||
errorMessage.includes('token') ||
errorMessage.includes('unauthorized') ||
errorMessage.includes('unauthenticated')
) {
return NextResponse.json(
{
error: 'Authentication failed. Please reconnect your Outlook account.',
authRequired: true,
details: errorMessage,
},
{ status: 401 }
)
}
throw innerError
}
} catch (error) {
logger.error('Error processing Outlook folders request:', error)
return NextResponse.json(
{
error: 'Failed to retrieve Outlook folders',
details: (error as Error).message,
},
{ status: 500 }
)
}
}