fix: eliminar footer rls en login y habilitar subida y sincronizacion de logo de cliente

This commit is contained in:
Marcos Fargas
2026-10-15 04:16:53 +02:00
parent 49d3529525
commit d049fa0f3b
5 changed files with 363 additions and 64 deletions

View File

@@ -12,18 +12,30 @@ import {
sanitizeFileName,
serviceSupabase,
} from '@/lib/assets';
import { randomUUID } from 'crypto';
import { randomUUID, createHash } from 'crypto';
import {
newId,
parseBrandbook,
type BrandAsset,
BRANDBOOK_DOC_TYPE,
BRANDBOOK_DOC_TITLE,
} from '@/lib/brandbook';
export const dynamic = 'force-dynamic';
export const runtime = 'nodejs';
const MAX_LOGO = 2 * 1024 * 1024; // 2 MB
const ALLOWED = new Set([
const MAX_LOGO = 15 * 1024 * 1024; // 15 MB
const ALLOWED_MIMES = new Set([
'image/png',
'image/jpeg',
'image/jpg',
'image/webp',
'image/svg+xml',
'image/svg',
'image/x-png',
'image/pjpeg',
]);
const ALLOWED_EXTS = new Set(['png', 'jpg', 'jpeg', 'webp', 'svg']);
async function requireAgency() {
const supabase = await createClient();
@@ -64,18 +76,36 @@ export async function POST(req: NextRequest) {
}
if (file.size > MAX_LOGO) {
return NextResponse.json(
{ error: 'Logo máximo 2 MB' },
{ error: 'Logo máximo 15 MB' },
{ status: 400 }
);
}
const mime = file.type || 'application/octet-stream';
if (!ALLOWED.has(mime)) {
const rawMime = (file.type || '').split(';')[0].trim().toLowerCase();
const ext = (file.name || '').split('.').pop()?.toLowerCase() || '';
const isAllowed =
ALLOWED_MIMES.has(rawMime) ||
rawMime.startsWith('image/') ||
ALLOWED_EXTS.has(ext);
if (!isAllowed) {
return NextResponse.json(
{ error: 'Usa PNG, JPG, WEBP o SVG' },
{ error: 'Formato no soportado. Usa PNG, JPG, WEBP o SVG.' },
{ status: 400 }
);
}
const resolvedMime =
rawMime && rawMime !== 'application/octet-stream'
? rawMime
: ext === 'svg'
? 'image/svg+xml'
: ext === 'webp'
? 'image/webp'
: ext === 'png'
? 'image/png'
: 'image/jpeg';
const bucketOk = await ensureAssetsBucket();
if (!bucketOk.ok) {
return NextResponse.json(
@@ -90,15 +120,51 @@ export async function POST(req: NextRequest) {
}
const safe = sanitizeFileName(file.name || 'logo.png');
const path = `${tenantId}/branding/${randomUUID()}_${safe}`;
const assetId = randomUUID();
const folder = 'branding';
const path = `${tenantId}/${folder}/${assetId}_${safe}`;
const buf = Buffer.from(await file.arrayBuffer());
const sha256 = createHash('sha256').update(buf).digest('hex');
const { error: upErr } = await sb.storage
.from(ASSETS_BUCKET)
.upload(path, buf, { contentType: mime, upsert: true });
.upload(path, buf, { contentType: resolvedMime, upsert: true });
if (upErr) {
return NextResponse.json({ error: upErr.message }, { status: 500 });
}
// Register in asset_files table so it can be previewed/downloaded cleanly
try {
const supabase = await createClient();
const {
data: { user },
} = await supabase.auth.getUser();
await fetch(`${supabaseUrl()}/rest/v1/asset_files`, {
method: 'POST',
headers: {
...headers,
'Content-Type': 'application/json',
Prefer: 'return=minimal',
},
body: JSON.stringify({
id: assetId,
tenant_id: tenantId,
title: safe,
folder,
file_name: safe,
mime_type: resolvedMime,
bytes: file.size,
storage_path: path,
sha256,
status: 'approved',
created_by: user?.id || null,
}),
});
} catch {
// Non-blocking
}
// Store storage path; served via /api/branding/logo
const logoUrl = `storage:${path}`;
const patch = await fetch(
@@ -125,6 +191,51 @@ export async function POST(req: NextRequest) {
);
}
// Also sync into documents (brandbook) kit_assets so it appears in Zona de marca
try {
const docRes = await fetch(
`${supabaseUrl()}/rest/v1/documents?tenant_id=eq.${encodeURIComponent(tenantId)}&type=eq.${BRANDBOOK_DOC_TYPE}&select=id,content&order=updated_at.desc&limit=1`,
{ headers, cache: 'no-store' }
);
if (docRes.ok) {
const docs = await docRes.json();
const doc = docs[0];
if (doc) {
const book = parseBrandbook(doc.content);
if (book) {
const asset: BrandAsset = {
id: newId('kit'),
asset_file_id: assetId,
kind: 'logo',
role: 'primary',
label: safe,
file_name: safe,
mime_type: resolvedMime,
added_at: new Date().toISOString(),
};
book.kit_assets = [
asset,
...(book.kit_assets || []).filter((a) => a.role !== 'primary'),
];
await fetch(`${supabaseUrl()}/rest/v1/documents?id=eq.${doc.id}`, {
method: 'PATCH',
headers: {
...headers,
'Content-Type': 'application/json',
Prefer: 'return=minimal',
},
body: JSON.stringify({
content: book,
updated_at: new Date().toISOString(),
}),
});
}
}
}
} catch {
// Non-blocking
}
return NextResponse.json({
logoUrl,
serveUrl: `/api/branding/logo?tenantId=${encodeURIComponent(tenantId)}`,

View File

@@ -235,6 +235,25 @@ export async function POST(req: NextRequest) {
);
}
if (kind === 'logo') {
try {
await fetch(
`${supabaseUrl()}/rest/v1/tenants?id=eq.${encodeURIComponent(ctx.tenantId)}`,
{
method: 'PATCH',
headers: {
...headers,
'Content-Type': 'application/json',
Prefer: 'return=minimal',
},
body: JSON.stringify({ logo_url: `storage:${storagePath}` }),
}
);
} catch {
// Non-blocking
}
}
return NextResponse.json({
ok: true,
asset,

View File

@@ -4,7 +4,7 @@ import React, { useState, Suspense } from 'react';
import Image from 'next/image';
import { useSearchParams } from 'next/navigation';
import { createClient } from '@/lib/supabase/client';
import { Mail, Lock, Eye, EyeOff, ShieldCheck, ArrowRight, Loader2 } from 'lucide-react';
import { Mail, Lock, Eye, EyeOff, ArrowRight, Loader2 } from 'lucide-react';
function LoginForm() {
const searchParams = useSearchParams();
@@ -172,15 +172,6 @@ export default function LoginPage() {
>
<LoginForm />
</Suspense>
<div className="mt-8 flex items-center justify-center gap-4 text-[11px] text-gray-500">
<div className="flex items-center gap-1.5">
<ShieldCheck className="w-3.5 h-3.5 text-emerald-500" />
<span>Aislamiento RLS multi-tenant</span>
</div>
<span>·</span>
<span>TLS 1.3</span>
</div>
</div>
</div>
);