Skip to content

Commit f89e0cb

Browse files
Thales-Chagasclaude
andcommitted
Seguranca: processar exige usuario logado de verdade (fecha abuso de custo de IA)
- verify_jwt aceitava a chave publica (anon), permitindo qualquer um na internet gastar as APIs de IA. Agora valida getUser() e rejeita a chave publica crua; adiciona rate limit por usuario (30/min). - erro 500 nao vaza mais detalhe interno (String(e)) - .gitignore passa a cobrir arquivos .env Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 556f439 commit f89e0cb

2 files changed

Lines changed: 37 additions & 1 deletion

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ src/dadosPlanilha.json
88
# Arquivos temporários do Supabase CLI (estado local)
99
supabase/.temp/
1010

11+
# Segredos em variáveis de ambiente — nunca publicar
12+
.env
13+
.env.*
14+
*.env
15+
1116
# Guia local do bot do Telegram — contém segredos (TELEGRAM_SECRET). Nunca publicar.
1217
PASSO-A-PASSO-TELEGRAM.md
1318

supabase/functions/processar/index.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,33 @@
88
// Segredos: ANTHROPIC_API_KEY, OPENAI_API_KEY
99
// ============================================================
1010

11+
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
12+
1113
const ANTHROPIC_API_KEY = Deno.env.get("ANTHROPIC_API_KEY")!;
1214
const OPENAI_API_KEY = Deno.env.get("OPENAI_API_KEY")!;
1315
const CLAUDE_MODEL = "claude-haiku-4-5-20251001";
1416

17+
// SUPABASE_URL e SUPABASE_ANON_KEY são injetados automaticamente pela plataforma.
18+
// Este cliente é usado SÓ para validar o usuário logado (getUser) — não lê dados.
19+
const authClient = createClient(
20+
Deno.env.get("SUPABASE_URL")!,
21+
Deno.env.get("SUPABASE_ANON_KEY")!,
22+
{ auth: { persistSession: false, autoRefreshToken: false } },
23+
);
24+
25+
// Limite de uso por usuário (anti-abuso de custo de IA). Em memória do isolate:
26+
// não é perfeito num ambiente serverless, mas é uma trava a mais além do login.
27+
const ACESSOS = new Map<string, number[]>();
28+
const LIMITE = 30; // requisições
29+
const JANELA = 60_000; // por 60s
30+
function dentroDoLimite(id: string): boolean {
31+
const agora = Date.now();
32+
const lista = (ACESSOS.get(id) || []).filter((t) => agora - t < JANELA);
33+
lista.push(agora);
34+
ACESSOS.set(id, lista);
35+
return lista.length <= LIMITE;
36+
}
37+
1538
const cors = {
1639
"Access-Control-Allow-Origin": "*",
1740
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
@@ -119,6 +142,14 @@ Deno.serve(async (req) => {
119142
if (req.method === "OPTIONS") return new Response("ok", { headers: cors });
120143
if (req.method !== "POST") return json({ erro: "use POST" }, 405);
121144

145+
// 🔒 Exige um usuário REAL logado. A chave pública (anon/publishable) fica
146+
// visível no app e, sozinha, passa no verify_jwt — mas aqui NÃO basta: quem
147+
// não tem sessão de usuário não gasta as APIs de IA. Fecha o abuso de custo.
148+
const token = (req.headers.get("Authorization") || "").replace(/^Bearer\s+/i, "");
149+
const { data: { user }, error: eAuth } = await authClient.auth.getUser(token);
150+
if (eAuth || !user) return json({ erro: "faça login para usar a IA" }, 401);
151+
if (!dentroDoLimite(user.id)) return json({ erro: "muitas requisições — tente em instantes" }, 429);
152+
122153
try {
123154
const { kind, base64, mime } = await req.json();
124155
if (!base64 || !kind) return json({ erro: "faltou kind ou base64" }, 400);
@@ -143,6 +174,6 @@ Deno.serve(async (req) => {
143174
return json({ erro: "kind inválido (use 'foto' ou 'audio')" }, 400);
144175
} catch (e) {
145176
console.error("erro:", e);
146-
return json({ ok: false, erro: String(e) }, 500);
177+
return json({ ok: false, erro: "erro ao processar" }, 500);
147178
}
148179
});

0 commit comments

Comments
 (0)