fix(security): harden auth, tenant scoping, and scrub secrets

Require session on APIs, scope KPIs/activos/simulate to the user tenant, strip real JWTs from .env.example, and add health/smoke/docs for 4Ventures ops.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-09-21 10:51:46 +02:00
parent e7b6ab71ca
commit 2df3fe6983
21 changed files with 621 additions and 276 deletions

View File

@@ -1,9 +1,28 @@
import { NextRequest } from 'next/server';
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
export async function POST(req: NextRequest) {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || 'https://supabase.4ventures.es';
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || '';
const tenantId = process.env.DEFAULT_TENANT_ID || '0b3c5fc0-6901-4caf-b26a-b6a0201d8541';
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY?.trim() || '';
const supabase = await createClient();
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const tenantId =
user.app_metadata?.tenant_id || user.user_metadata?.tenant_id || null;
if (!tenantId) {
return NextResponse.json(
{ error: 'Usuario sin tenant_id' },
{ status: 403 }
);
}
const keyForRpc = serviceKey || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || '';
try {
const body = await req.json();
@@ -13,25 +32,25 @@ export async function POST(req: NextRequest) {
const res = await fetch(`${supabaseUrl}/rest/v1/rpc/simulate_budget`, {
method: 'POST',
headers: {
apikey: serviceKey,
Authorization: `Bearer ${serviceKey}`,
'Content-Type': 'application/json'
apikey: keyForRpc,
Authorization: `Bearer ${keyForRpc}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
p_tenant_id: tenantId,
p_additional_spend: spend,
p_target_channel: channel === 'blended' ? null : channel
})
p_target_channel: channel === 'blended' ? null : channel,
}),
});
if (res.ok) {
const data = await res.json();
return Response.json(data);
} else {
const err = await res.text();
return Response.json({ error: err }, { status: 500 });
return NextResponse.json(data);
}
} catch (e: any) {
return Response.json({ error: e.message }, { status: 500 });
const err = await res.text();
return NextResponse.json({ error: err }, { status: 500 });
} catch (e: unknown) {
const message = e instanceof Error ? e.message : 'Error';
return NextResponse.json({ error: message }, { status: 500 });
}
}