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:
4Ventures
2026-10-13 17:48:47 +02:00
parent 1cf8e7c1bd
commit d3e383dd08
5 changed files with 310 additions and 109 deletions

View File

@@ -15,6 +15,10 @@ import {
sanitizeFolder,
serviceSupabase,
} from '@/lib/assets';
import {
filterPortalAssetsForClient,
isAppGeneratedPortalFolder,
} from '@/lib/assets-client-scope';
import {
ensureChildFolder,
getTenantDriveFolderId,
@@ -64,12 +68,18 @@ export async function GET() {
const msg = await r.text();
return NextResponse.json({
assets: [],
agency: true,
clientScope: 'full',
warning: msg.includes('does not exist')
? 'Aplicar sql/asset_files.sql'
: msg.slice(0, 200),
});
}
return NextResponse.json({ assets: await r.json() });
return NextResponse.json({
assets: await r.json(),
agency: true,
clientScope: 'full',
});
}
const supabase = await createClient();
@@ -81,19 +91,27 @@ export async function GET() {
if (error) {
return NextResponse.json({
assets: [],
agency: false,
clientScope: 'app_generated',
warning: error.message,
});
}
return NextResponse.json({ assets: data ?? [] });
// Client JWT path: still filter to app-generated folders only
return NextResponse.json({
assets: filterPortalAssetsForClient(data ?? []),
agency: false,
clientScope: 'app_generated',
});
}
/** POST multipart: files[] + optional folder */
export async function POST(req: NextRequest) {
const g = await gate();
if ('error' in g && g.error) return g.error;
const { user, ctx } = g as Awaited<ReturnType<typeof gate>> & {
const { user, ctx, agency } = g as Awaited<ReturnType<typeof gate>> & {
user: { id: string };
ctx: { tenantId: string };
agency: boolean;
};
const bucketOk = await ensureAssetsBucket();
@@ -115,6 +133,15 @@ export async function POST(req: NextRequest) {
const form = await req.formData();
const folder = sanitizeFolder(String(form.get('folder') || ''));
if (!agency && !isAppGeneratedPortalFolder(folder)) {
return NextResponse.json(
{
error:
'Como cliente solo puedes usar carpetas generadas por la app (creatividades, calendarios, brandbook…).',
},
{ status: 403 }
);
}
const files = form.getAll('files').filter((f): f is File => f instanceof File);
if (!files.length) {
@@ -234,10 +261,18 @@ export async function POST(req: NextRequest) {
export async function DELETE(req: NextRequest) {
const g = await gate();
if ('error' in g && g.error) return g.error;
const { ctx } = g as Awaited<ReturnType<typeof gate>> & {
const { ctx, agency } = g as Awaited<ReturnType<typeof gate>> & {
ctx: { tenantId: string };
agency: boolean;
};
if (!agency) {
return NextResponse.json(
{ error: 'Solo la agencia puede eliminar archivos de la biblioteca' },
{ status: 403 }
);
}
const id = req.nextUrl.searchParams.get('id')?.trim();
if (!id) {
return NextResponse.json({ error: 'id requerido' }, { status: 400 });