mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-28 03:00:29 -04:00
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:
@@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
220
apps/sim/app/api/auth/oauth/connections/route.test.ts
Normal file
220
apps/sim/app/api/auth/oauth/connections/route.test.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
256
apps/sim/app/api/auth/oauth/credentials/route.test.ts
Normal file
256
apps/sim/app/api/auth/oauth/credentials/route.test.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
159
apps/sim/app/api/auth/oauth/disconnect/route.test.ts
Normal file
159
apps/sim/app/api/auth/oauth/disconnect/route.test.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
@@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user