Limit client asset library to app-generated folders only.
Agency still sees the full Drive tree and portal uploads; clients only see Pipeline-creatividades / calendarios / brandbook-style paths. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,6 +2,10 @@ 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,
|
||||
@@ -15,7 +19,33 @@ export const dynamic = 'force-dynamic';
|
||||
|
||||
const FOLDER_MIME = 'application/vnd.google-apps.folder';
|
||||
|
||||
/** GET — exact replica of the tenant's Drive folder tree (under Clientes). */
|
||||
async function listAppGeneratedRootFolderIds(
|
||||
clientRootId: string
|
||||
): Promise<string[]> {
|
||||
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<boolean> {
|
||||
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 {
|
||||
@@ -33,7 +63,7 @@ export async function GET(req: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
void isAgencyRole(userRole(user));
|
||||
const agency = isAgencyRole(userRole(user));
|
||||
|
||||
const downloadId = req.nextUrl.searchParams.get('download')?.trim();
|
||||
if (downloadId) {
|
||||
@@ -52,6 +82,15 @@ export async function GET(req: NextRequest) {
|
||||
{ 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) {
|
||||
@@ -67,6 +106,8 @@ export async function GET(req: NextRequest) {
|
||||
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».',
|
||||
});
|
||||
@@ -88,6 +129,7 @@ export async function GET(req: NextRequest) {
|
||||
|
||||
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);
|
||||
@@ -102,13 +144,32 @@ export async function GET(req: NextRequest) {
|
||||
{ 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);
|
||||
const mapped = files
|
||||
let mapped = files
|
||||
.map((f) => ({
|
||||
id: f.id,
|
||||
name: f.name,
|
||||
@@ -124,19 +185,34 @@ export async function GET(req: NextRequest) {
|
||||
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: `https://drive.google.com/drive/folders/${encodeURIComponent(folderId)}`,
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user