import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { googleDriveFileSelectorContract } from '@/lib/api/contracts/selectors/google' import { parseRequest } from '@/lib/api/server' import { authorizeCredentialUse } from '@/lib/auth/credential-access' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { validateAlphanumericId } from '@/lib/core/security/input-validation' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { getScopesForService } from '@/lib/oauth/utils' import { refreshAccessTokenIfNeeded, ServiceAccountTokenError } from '@/app/api/auth/oauth/utils' export const dynamic = 'force-dynamic' const logger = createLogger('GoogleDriveFileAPI') /** * Get a single file from Google Drive */ export const GET = withRouteHandler(async (request: NextRequest) => { const requestId = generateRequestId() logger.info(`[${requestId}] Google Drive file request received`) const auth = await checkSessionOrInternalAuth(request) if (!auth.success || !auth.userId) { return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) } try { const parsed = await parseRequest( googleDriveFileSelectorContract, request, {}, { validationErrorResponse: () => { logger.warn(`[${requestId}] Missing required parameters`) return NextResponse.json( { error: 'Credential ID and File ID are required' }, { status: 400 } ) }, } ) if (!parsed.success) return parsed.response const { credentialId, fileId } = parsed.data.query const workflowId = parsed.data.query.workflowId || undefined const impersonateEmail = parsed.data.query.impersonateEmail || undefined const fileIdValidation = validateAlphanumericId(fileId, 'fileId', 255) if (!fileIdValidation.isValid) { logger.warn(`[${requestId}] Invalid file ID: ${fileIdValidation.error}`) return NextResponse.json({ error: fileIdValidation.error }, { status: 400 }) } const authz = await authorizeCredentialUse(request, { credentialId, workflowId }) if (!authz.ok || !authz.credentialOwnerUserId) { return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 }) } const accessToken = await refreshAccessTokenIfNeeded( credentialId, authz.credentialOwnerUserId, requestId, getScopesForService('google-drive'), impersonateEmail ) if (!accessToken) { return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 }) } 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,shortcutDetails&supportsAllDrives=true`, { headers: { Authorization: `Bearer ${accessToken}`, }, } ) if (!response.ok && response.status === 404) { logger.info(`[${requestId}] File not found, checking if it's a shared drive`) const driveResponse = await fetch( `https://www.googleapis.com/drive/v3/drives/${fileId}?fields=id,name`, { headers: { Authorization: `Bearer ${accessToken}`, }, } ) if (driveResponse.ok) { const driveData = await driveResponse.json() logger.info(`[${requestId}] Found shared drive: ${driveData.name}`) return NextResponse.json( { file: { id: driveData.id, name: driveData.name, mimeType: 'application/vnd.google-apps.folder', iconLink: 'https://ssl.gstatic.com/docs/doclist/images/icon_11_shared_collection_list_1.png', }, }, { status: 200 } ) } } 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() const exportFormats: { [key: string]: string } = { 'application/vnd.google-apps.document': 'application/pdf', 'application/vnd.google-apps.spreadsheet': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.google-apps.presentation': 'application/pdf', } if ( file.mimeType === 'application/vnd.google-apps.shortcut' && file.shortcutDetails?.targetId ) { const targetId = file.shortcutDetails.targetId const shortcutResp = await fetch( `https://www.googleapis.com/drive/v3/files/${targetId}?fields=id,name,mimeType,iconLink,webViewLink,thumbnailLink,createdTime,modifiedTime,size,owners,exportLinks&supportsAllDrives=true`, { headers: { Authorization: `Bearer ${accessToken}` }, } ) if (shortcutResp.ok) { const targetFile = await shortcutResp.json() file.id = targetFile.id file.name = targetFile.name file.mimeType = targetFile.mimeType file.iconLink = targetFile.iconLink file.webViewLink = targetFile.webViewLink file.thumbnailLink = targetFile.thumbnailLink file.createdTime = targetFile.createdTime file.modifiedTime = targetFile.modifiedTime file.size = targetFile.size file.owners = targetFile.owners file.exportLinks = targetFile.exportLinks } } if (file.mimeType.startsWith('application/vnd.google-apps.')) { const format = exportFormats[file.mimeType] || 'application/pdf' if (!file.exportLinks) { file.downloadUrl = `https://www.googleapis.com/drive/v3/files/${file.id}/export?mimeType=${encodeURIComponent( format )}&supportsAllDrives=true` } else { file.downloadUrl = file.exportLinks[format] } } else { file.downloadUrl = `https://www.googleapis.com/drive/v3/files/${file.id}?alt=media&supportsAllDrives=true` } return NextResponse.json({ file }, { status: 200 }) } catch (error) { if (error instanceof ServiceAccountTokenError) { logger.warn(`[${requestId}] Service account token error`, { message: error.message }) return NextResponse.json({ error: error.message }, { status: 400 }) } logger.error(`[${requestId}] Error fetching file from Google Drive`, error) return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } })