import { NextRequest, NextResponse } from 'next/server'; import { createClient } from '@/lib/supabase/server'; import { isAgencyRole, userRole } from '@/lib/agency'; import { resolveActiveTenant } from '@/lib/tenant-context'; import { filterDriveEntriesForClientRoot, isAppGeneratedDriveFolderName, } from '@/lib/assets-client-scope'; import { findClientesRootFolder, getDriveDownloadUrl, getDriveFileMeta, getTenantDriveFolderId, isUnderDriveFolder, listFilesInFolder, } from '@/lib/google-drive'; export const dynamic = 'force-dynamic'; const FOLDER_MIME = 'application/vnd.google-apps.folder'; async function listAppGeneratedRootFolderIds( clientRootId: string ): Promise { const rootFiles = await listFilesInFolder(clientRootId); return rootFiles .filter( (f) => f.mimeType === FOLDER_MIME && isAppGeneratedDriveFolderName(f.name) ) .map((f) => f.id); } async function isUnderAppGeneratedTree( fileOrFolderId: string, clientRootId: string, appRootIds?: string[] ): Promise { const roots = appRootIds ?? (await listAppGeneratedRootFolderIds(clientRootId)); for (const appId of roots) { if (fileOrFolderId === appId) return true; if (await isUnderDriveFolder(fileOrFolderId, appId)) return true; } return false; } /** GET — Drive tree for active tenant. Agency: full tree. Client: app folders only. */ export async function GET(req: NextRequest) { const supabase = await createClient(); const { data: { user }, } = await supabase.auth.getUser(); if (!user) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const ctx = await resolveActiveTenant(user); if (!ctx.tenantId || ctx.portfolioWide) { return NextResponse.json( { error: 'Selecciona un cliente en Contexto' }, { status: 400 } ); } const agency = isAgencyRole(userRole(user)); const downloadId = req.nextUrl.searchParams.get('download')?.trim(); if (downloadId) { try { const link = await getTenantDriveFolderId(ctx.tenantId); if (!link) { return NextResponse.json( { error: 'Sin carpeta Drive del cliente' }, { status: 400 } ); } const ok = await isUnderDriveFolder(downloadId, link.folderId); if (!ok) { return NextResponse.json( { error: 'Archivo fuera de la carpeta del cliente' }, { status: 403 } ); } if (!agency) { const inApp = await isUnderAppGeneratedTree(downloadId, link.folderId); if (!inApp) { return NextResponse.json( { error: 'Archivo fuera del alcance del cliente' }, { status: 403 } ); } } const url = await getDriveDownloadUrl(downloadId); return NextResponse.json({ url }); } catch (e) { return NextResponse.json( { error: e instanceof Error ? e.message : 'download failed' }, { status: 500 } ); } } const link = await getTenantDriveFolderId(ctx.tenantId); if (!link) { return NextResponse.json({ files: [], linked: false, agency, clientScope: agency ? 'full' : 'app_generated', warning: 'Sin carpeta Drive del cliente. En Agencia → auto-match bajo la carpeta «Clientes».', }); } // Soft check: linked folder should live under «Clientes» let scopeWarning: string | null = null; try { const clientes = await findClientesRootFolder(); if (clientes) { const ok = await isUnderDriveFolder(link.folderId, clientes.id); if (!ok) { scopeWarning = `La carpeta enlazada («${link.folderName || link.folderId}») no está dentro de «Clientes». Rematch en Agencia.`; } } } catch { /* ignore */ } const requested = req.nextUrl.searchParams.get('folderId')?.trim(); let folderId = link.folderId; let appRootIds: string[] | undefined; if (requested && requested !== link.folderId) { const allowed = await isUnderDriveFolder(requested, link.folderId); if (!allowed) { return NextResponse.json( { error: 'Carpeta fuera del árbol del cliente. Solo se permite navegar dentro de su carpeta en Clientes.', rootFolderId: link.folderId, folderName: link.folderName, }, { status: 403 } ); } if (!agency) { appRootIds = await listAppGeneratedRootFolderIds(link.folderId); const inApp = await isUnderAppGeneratedTree( requested, link.folderId, appRootIds ); if (!inApp) { return NextResponse.json( { error: 'Solo puedes abrir carpetas generadas por el Centro de Mando (creatividades, calendarios…).', rootFolderId: link.folderId, folderName: link.folderName, }, { status: 403 } ); } } folderId = requested; } try { const currentMeta = await getDriveFileMeta(folderId); const files = await listFilesInFolder(folderId); let mapped = files .map((f) => ({ id: f.id, name: f.name, mimeType: f.mimeType, bytes: f.size ? Number(f.size) : null, modifiedTime: f.modifiedTime, webViewLink: f.webViewLink, isFolder: f.mimeType === FOLDER_MIME, source: 'drive' as const, })) .sort((a, b) => { if (a.isFolder !== b.isFolder) return a.isFolder ? -1 : 1; return a.name.localeCompare(b.name, 'es'); }); const atClientRoot = folderId === link.folderId; if (!agency && atClientRoot) { mapped = filterDriveEntriesForClientRoot(mapped); if (!mapped.length && !scopeWarning) { scopeWarning = 'Aún no hay carpetas generadas por la app (p. ej. Pipeline-creatividades). El resto del Drive solo lo ve la agencia.'; } } return NextResponse.json({ linked: true, agency, clientScope: agency ? 'full' : 'app_generated', rootFolderId: link.folderId, rootFolderName: link.folderName, folderId, folderName: currentMeta?.name || link.folderName, driveOpenUrl: agency ? `https://drive.google.com/drive/folders/${encodeURIComponent(folderId)}` : null, files: mapped, warning: scopeWarning, }); } catch (e) { return NextResponse.json({ linked: true, agency, clientScope: agency ? 'full' : 'app_generated', rootFolderId: link.folderId, rootFolderName: link.folderName, folderId, folderName: link.folderName, files: [], warning: scopeWarning || (e instanceof Error ? e.message : 'Drive error'), }); } }