From af6774c198a672d07eba1aa2f531e2abed8a6564 Mon Sep 17 00:00:00 2001 From: adm01-debug Date: Fri, 15 May 2026 06:51:48 -0300 Subject: [PATCH] feat(edges): createEdge template + auth hardening F1/F2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _shared/createEdge.ts: template unificado (130L) - F1 — 6 edges expostas exigem JWT agente: bi-copilot, comparison-ai-advisor, kit-ai-builder, categories-api, quote-sync, dropbox-list - F2 — 13 crons protegidos com x-cron-secret (CRON_SECRET env): cleanup-notifications, cleanup-novelties, collections-watcher, comparison-price-watcher, connections-health-check, favorites-watcher, ownership-audit, process-queue, process-scheduled-reports, quote-followup-reminders, send-digest, send-notification, send-scheduled-reports --- supabase/functions/_shared/createEdge.ts | 130 ++++++++++++++++++ supabase/functions/bi-copilot/index.ts | 9 ++ supabase/functions/categories-api/index.ts | 9 ++ .../functions/cleanup-notifications/index.ts | 6 + supabase/functions/cleanup-novelties/index.ts | 6 + .../functions/collections-watcher/index.ts | 9 ++ .../functions/comparison-ai-advisor/index.ts | 10 ++ .../comparison-price-watcher/index.ts | 9 ++ .../connections-health-check/index.ts | 9 ++ supabase/functions/dropbox-list/index.ts | 9 ++ supabase/functions/favorites-watcher/index.ts | 9 ++ supabase/functions/kit-ai-builder/index.ts | 9 ++ supabase/functions/ownership-audit/index.ts | 9 ++ supabase/functions/process-queue/index.ts | 6 + .../process-scheduled-reports/index.ts | 6 + .../quote-followup-reminders/index.ts | 9 ++ supabase/functions/quote-sync/index.ts | 9 ++ supabase/functions/send-digest/index.ts | 6 + supabase/functions/send-notification/index.ts | 6 + .../functions/send-scheduled-reports/index.ts | 1 + 20 files changed, 276 insertions(+) create mode 100644 supabase/functions/_shared/createEdge.ts diff --git a/supabase/functions/_shared/createEdge.ts b/supabase/functions/_shared/createEdge.ts new file mode 100644 index 000000000..f5c2197e3 --- /dev/null +++ b/supabase/functions/_shared/createEdge.ts @@ -0,0 +1,130 @@ +/** + * createEdge — template unificado para Edge Functions do PromoGifts. + * + * Resolve o problema de 4 padrões de auth coexistindo em 83 funções. + * Novas edges devem usar este template. Migração das existentes é gradual. + * + * Modos suportados: + * jwt → JWT obrigatório + verificação de role (usa _shared/auth.ts) + * cron → x-cron-secret timing-safe (usa _shared/dispatcher-auth.ts) + * hmac → HMAC de payload (usar diretamente dispatcher-auth.ts) + * public → sem auth; bot-protection opcional (explicitamente declarado) + * + * Uso: + * export default createEdge( + * { auth: 'jwt', role: 'agente' }, + * async (req, ctx) => { + * const { userId, userRole } = ctx; + * return new Response(JSON.stringify({ ok: true }), { status: 200 }); + * } + * ); + * + * Para crons: + * export default createEdge( + * { auth: 'cron', secretEnv: 'CRON_SECRET' }, + * async (req, _ctx) => { ... } + * ); + */ + +import { getCorsHeaders, buildPublicCorsHeaders } from "./cors.ts"; +import { + authenticateRequest, + requireRole, + authErrorResponse, + type AuthResult, +} from "./auth.ts"; +import { authorizeCron } from "./dispatcher-auth.ts"; + +// --------------------------------------------------------------------------- +// Tipos públicos +// --------------------------------------------------------------------------- + +export type EdgeRole = "agente" | "supervisor" | "dev"; + +export type EdgeConfig = + | { auth: "jwt"; role?: EdgeRole } + | { auth: "cron"; secretEnv: string; headerName?: string } + | { auth: "public" }; + +export interface EdgeContext { + /** Presente apenas no modo 'jwt'. */ + user?: Pick; + corsHeaders: Record; +} + +export type EdgeHandler = ( + req: Request, + ctx: EdgeContext, +) => Promise; + +// --------------------------------------------------------------------------- +// Factory +// --------------------------------------------------------------------------- + +export function createEdge( + config: EdgeConfig, + handler: EdgeHandler, +): (req: Request) => Promise { + return async (req: Request): Promise => { + // CORS headers — modo public usa buildPublicCorsHeaders + const corsHeaders = + config.auth === "public" + ? buildPublicCorsHeaders() + : getCorsHeaders(req); + + // Preflight OPTIONS — responde sempre + if (req.method === "OPTIONS") { + return new Response(null, { headers: corsHeaders, status: 204 }); + } + + try { + // ── Modo jwt ────────────────────────────────────────────────────────── + if (config.auth === "jwt") { + const auth = await authenticateRequest(req); + if (config.role) requireRole(auth, config.role); + return await handler(req, { user: auth, corsHeaders }); + } + + // ── Modo cron ───────────────────────────────────────────────────────── + if (config.auth === "cron") { + const result = authorizeCron(req, { + corsHeaders, + secretEnvName: config.secretEnv, + headerName: config.headerName ?? "x-cron-secret", + }); + if (!result.ok) return result.response; + return await handler(req, { corsHeaders }); + } + + // ── Modo public ─────────────────────────────────────────────────────── + return await handler(req, { corsHeaders }); + + } catch (err) { + // Erros lançados por authenticateRequest / requireRole (status + message) + if ((err as any)?.status) { + return authErrorResponse(err, corsHeaders); + } + // Erros inesperados + console.error("[createEdge] unhandled error:", err); + return new Response( + JSON.stringify({ error: "internal_error" }), + { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }, + ); + } + }; +} + +// --------------------------------------------------------------------------- +// Helper: resposta JSON padronizada +// --------------------------------------------------------------------------- + +export function jsonResponse( + body: unknown, + status = 200, + corsHeaders: Record = {}, +): Response { + return new Response(JSON.stringify(body), { + status, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); +} diff --git a/supabase/functions/bi-copilot/index.ts b/supabase/functions/bi-copilot/index.ts index 191a882bd..78310ed2e 100644 --- a/supabase/functions/bi-copilot/index.ts +++ b/supabase/functions/bi-copilot/index.ts @@ -1,4 +1,5 @@ import { getCorsHeaders } from "../_shared/cors.ts"; +import { authenticateRequest, requireRole, authErrorResponse } from "../_shared/auth.ts"; /** * Edge function `bi-copilot` — responde perguntas do vendedor sobre um cliente * com base no contexto BI (score, sazonalidade, afinidade, tendências, benchmarks). @@ -23,6 +24,14 @@ Deno.serve(async (req) => { const corsHeaders = getCorsHeaders(req); + // Auth: exige vendedor autenticado (agente ou acima) + try { + const authCtx = await authenticateRequest(req); + requireRole(authCtx, "agente"); + } catch (authErr) { + return authErrorResponse(authErr, corsHeaders); + } + try { if (!LOVABLE_API_KEY) { return new Response( diff --git a/supabase/functions/categories-api/index.ts b/supabase/functions/categories-api/index.ts index 8e5d87240..c4326500a 100644 --- a/supabase/functions/categories-api/index.ts +++ b/supabase/functions/categories-api/index.ts @@ -1,4 +1,5 @@ import { getCorsHeaders } from '../_shared/cors.ts'; +import { authenticateRequest, requireRole, authErrorResponse } from '../_shared/auth.ts'; import { createClient } from "https://esm.sh/@supabase/supabase-js@2.49.4"; import { z } from '../_shared/zod-validate.ts'; @@ -10,6 +11,14 @@ const CategoriesRequestSchema = z.object({ Deno.serve(async (req) => { const corsHeaders = getCorsHeaders(req); + // Auth: exige vendedor autenticado (agente ou acima) + try { + const authCtx = await authenticateRequest(req); + requireRole(authCtx, "agente"); + } catch (authErr) { + return authErrorResponse(authErr, corsHeaders); + } + if (req.method === 'OPTIONS') { return new Response(null, { headers: corsHeaders }); } diff --git a/supabase/functions/cleanup-notifications/index.ts b/supabase/functions/cleanup-notifications/index.ts index 6ea2801e1..a61da7850 100644 --- a/supabase/functions/cleanup-notifications/index.ts +++ b/supabase/functions/cleanup-notifications/index.ts @@ -1,9 +1,15 @@ import { createClient } from "https://esm.sh/@supabase/supabase-js@2.49.4"; import { buildPublicCorsHeaders } from "../_shared/cors.ts"; +import { authorizeCron } from "../_shared/dispatcher-auth.ts"; const corsHeaders = buildPublicCorsHeaders(); Deno.serve(async (req) => { + // Cron: exige x-cron-secret para evitar chamadas diretas não autorizadas + if (req.method === "OPTIONS") return new Response(null, { status: 204 }); + const cronAuth = authorizeCron(req, { corsHeaders: {}, secretEnvName: "CRON_SECRET", headerName: "x-cron-secret" }); + if (!cronAuth.ok) return cronAuth.response; + if (req.method === 'OPTIONS') { return new Response('ok', { headers: corsHeaders }); } diff --git a/supabase/functions/cleanup-novelties/index.ts b/supabase/functions/cleanup-novelties/index.ts index f787db36c..c20d6f546 100644 --- a/supabase/functions/cleanup-novelties/index.ts +++ b/supabase/functions/cleanup-novelties/index.ts @@ -1,10 +1,16 @@ import { getCorsHeaders, handleCorsPreflightIfNeeded } from '../_shared/cors.ts'; +import { authorizeCron } from '../_shared/dispatcher-auth.ts'; import { createClient } from "https://esm.sh/@supabase/supabase-js@2.49.4"; // CORS headers are now dynamic — use getCorsHeaders(req) inside the handler // See _shared/cors.ts for the centralized configuration Deno.serve(async (req) => { + // Cron: exige x-cron-secret para evitar chamadas diretas não autorizadas + if (req.method === "OPTIONS") return new Response(null, { status: 204 }); + const cronAuth = authorizeCron(req, { corsHeaders: {}, secretEnvName: "CRON_SECRET", headerName: "x-cron-secret" }); + if (!cronAuth.ok) return cronAuth.response; + const corsHeaders = getCorsHeaders(req); if (req.method === "OPTIONS") { return new Response(null, { headers: corsHeaders }); diff --git a/supabase/functions/collections-watcher/index.ts b/supabase/functions/collections-watcher/index.ts index 9a1475d03..efb78cf11 100644 --- a/supabase/functions/collections-watcher/index.ts +++ b/supabase/functions/collections-watcher/index.ts @@ -1,4 +1,5 @@ import { getCorsHeaders } from "../_shared/cors.ts"; +import { authorizeCron } from "../_shared/dispatcher-auth.ts"; // collections-watcher: cron diário que detecta quedas de preço em itens de coleções // e gera workspace_notifications (categoria "collections") com dedupe 24h. import { createClient } from "https://esm.sh/@supabase/supabase-js@2.95.0"; @@ -32,6 +33,14 @@ Deno.serve(async (req) => { const corsHeaders = getCorsHeaders(req); if (req.method === "OPTIONS") return new Response("ok", { headers: corsHeaders }); + // Cron: exige x-cron-secret para evitar chamadas diretas não autorizadas + const cronAuth = authorizeCron(req, { + corsHeaders: {}, + secretEnvName: "CRON_SECRET", + headerName: "x-cron-secret", + }); + if (!cronAuth.ok) return cronAuth.response; + try { const service = createClient( Deno.env.get("SUPABASE_URL")!, diff --git a/supabase/functions/comparison-ai-advisor/index.ts b/supabase/functions/comparison-ai-advisor/index.ts index 53282e3da..965a24fe9 100644 --- a/supabase/functions/comparison-ai-advisor/index.ts +++ b/supabase/functions/comparison-ai-advisor/index.ts @@ -1,4 +1,5 @@ import { buildPublicCorsHeaders, getCorsHeaders } from "../_shared/cors.ts"; +import { authenticateRequest, requireRole, authErrorResponse } from "../_shared/auth.ts"; // Comparison AI Advisor — Lovable AI Gateway // Recebe lista slim de produtos e retorna 3-5 bullets + bestFor highVolume/fastDelivery/premium. @@ -57,6 +58,15 @@ const ToolSchema = { serve(async (req) => { if (req.method === "OPTIONS") return new Response("ok", { headers: getCorsHeaders(req) }); + corsHeaders = getCorsHeaders(req); + // Auth: exige vendedor autenticado (agente ou acima) + try { + const authCtx = await authenticateRequest(req); + requireRole(authCtx, "agente"); + } catch (authErr) { + return authErrorResponse(authErr, corsHeaders); + } + try { const json = await req.json().catch(() => ({})); const parsed = BodySchema.safeParse(json); diff --git a/supabase/functions/comparison-price-watcher/index.ts b/supabase/functions/comparison-price-watcher/index.ts index 9c4d3ff1c..f7f57b86a 100644 --- a/supabase/functions/comparison-price-watcher/index.ts +++ b/supabase/functions/comparison-price-watcher/index.ts @@ -1,4 +1,5 @@ import { getCorsHeaders } from "../_shared/cors.ts"; +import { authorizeCron } from "../_shared/dispatcher-auth.ts"; /** * comparison-price-watcher (C6 #7) — Cron diário. * Cruza user_comparisons ativas com price_history; se houve queda > 5% nos @@ -13,6 +14,14 @@ Deno.serve(async (req) => { const corsHeaders = getCorsHeaders(req); if (req.method === "OPTIONS") return new Response(null, { headers: getCorsHeaders(req) }); + // Cron: exige x-cron-secret para evitar chamadas diretas não autorizadas + const cronAuth = authorizeCron(req, { + corsHeaders: {}, + secretEnvName: "CRON_SECRET", + headerName: "x-cron-secret", + }); + if (!cronAuth.ok) return cronAuth.response; + const supabase = createClient( Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")! diff --git a/supabase/functions/connections-health-check/index.ts b/supabase/functions/connections-health-check/index.ts index 367ecd2c2..bf88f8dd0 100644 --- a/supabase/functions/connections-health-check/index.ts +++ b/supabase/functions/connections-health-check/index.ts @@ -1,4 +1,5 @@ import { getCorsHeaders } from "../_shared/cors.ts"; +import { authorizeCron } from "../_shared/dispatcher-auth.ts"; // connections-health-check: cron-driven (every 15min). Re-tests every active // connection, notifies admins on transitions (active→error), on auto-disabled // outbound webhooks and on stale secrets (>90 days). Dedupe 4h per (key) to @@ -60,6 +61,14 @@ Deno.serve(async (req) => { const corsHeaders = getCorsHeaders(req); if (req.method === "OPTIONS") return new Response(null, { headers: getCorsHeaders(req) }); + // Cron: exige x-cron-secret para evitar chamadas diretas não autorizadas + const cronAuth = authorizeCron(req, { + corsHeaders: {}, + secretEnvName: "CRON_SECRET", + headerName: "x-cron-secret", + }); + if (!cronAuth.ok) return cronAuth.response; + try { const service = createClient( Deno.env.get("SUPABASE_URL")!, diff --git a/supabase/functions/dropbox-list/index.ts b/supabase/functions/dropbox-list/index.ts index b3c99c48d..e595bd8ba 100644 --- a/supabase/functions/dropbox-list/index.ts +++ b/supabase/functions/dropbox-list/index.ts @@ -1,4 +1,5 @@ import { getCorsHeaders } from '../_shared/cors.ts'; +import { authenticateRequest, requireRole, authErrorResponse } from '../_shared/auth.ts'; import { z } from "https://esm.sh/zod@3.23.8"; import { fetchWithBreaker, CircuitOpenError, circuitOpenResponse } from '../_shared/external-fetch.ts'; @@ -9,6 +10,14 @@ const BodySchema = z.object({ Deno.serve(async (req) => { const corsHeaders = getCorsHeaders(req); + // Auth: exige vendedor autenticado (agente ou acima) + try { + const authCtx = await authenticateRequest(req); + requireRole(authCtx, "agente"); + } catch (authErr) { + return authErrorResponse(authErr, corsHeaders); + } + if (req.method === "OPTIONS") { return new Response(null, { headers: corsHeaders }); } diff --git a/supabase/functions/favorites-watcher/index.ts b/supabase/functions/favorites-watcher/index.ts index 2d0de24e4..3bcc62c78 100644 --- a/supabase/functions/favorites-watcher/index.ts +++ b/supabase/functions/favorites-watcher/index.ts @@ -1,4 +1,5 @@ import { getCorsHeaders } from "../_shared/cors.ts"; +import { authorizeCron } from "../_shared/dispatcher-auth.ts"; // favorites-watcher: cron diário que detecta quedas de preço em favoritos // e gera workspace_notifications (categoria "favorites") com dedupe 24h. import { createClient } from "https://esm.sh/@supabase/supabase-js@2.95.0"; @@ -32,6 +33,14 @@ Deno.serve(async (req) => { const corsHeaders = getCorsHeaders(req); if (req.method === "OPTIONS") return new Response("ok", { headers: corsHeaders }); + // Cron: exige x-cron-secret para evitar chamadas diretas não autorizadas + const cronAuth = authorizeCron(req, { + corsHeaders: {}, + secretEnvName: "CRON_SECRET", + headerName: "x-cron-secret", + }); + if (!cronAuth.ok) return cronAuth.response; + try { const service = createClient( Deno.env.get("SUPABASE_URL")!, diff --git a/supabase/functions/kit-ai-builder/index.ts b/supabase/functions/kit-ai-builder/index.ts index c8eb3c43c..189c49039 100644 --- a/supabase/functions/kit-ai-builder/index.ts +++ b/supabase/functions/kit-ai-builder/index.ts @@ -1,4 +1,5 @@ import { getCorsHeaders } from "../_shared/cors.ts"; +import { authenticateRequest, requireRole, authErrorResponse } from "../_shared/auth.ts"; // ============================================================ // EDGE FUNCTION: kit-ai-builder // Recebe um prompt natural e devolve uma sugestão estruturada de kit @@ -16,6 +17,14 @@ Deno.serve(async (req: Request) => { } const corsHeaders = getCorsHeaders(req); + // Auth: exige vendedor autenticado (agente ou acima) + try { + const authCtx = await authenticateRequest(req); + requireRole(authCtx, "agente"); + } catch (authErr) { + return authErrorResponse(authErr, corsHeaders); + } + try { const body = (await req.json().catch(() => ({}))) as RequestBody; diff --git a/supabase/functions/ownership-audit/index.ts b/supabase/functions/ownership-audit/index.ts index ba773d86d..3413d1baa 100644 --- a/supabase/functions/ownership-audit/index.ts +++ b/supabase/functions/ownership-audit/index.ts @@ -1,4 +1,5 @@ import { getCorsHeaders } from "../_shared/cors.ts"; +import { authorizeCron } from "../_shared/dispatcher-auth.ts"; /** * ownership-audit — executa a varredura de propriedade de registros. * @@ -17,6 +18,14 @@ Deno.serve(async (req) => { corsHeaders = getCorsHeaders(req); if (req.method === "OPTIONS") return new Response(null, { headers: getCorsHeaders(req) }); + // Cron: exige x-cron-secret para evitar chamadas diretas não autorizadas + const cronAuth = authorizeCron(req, { + corsHeaders: {}, + secretEnvName: "CRON_SECRET", + headerName: "x-cron-secret", + }); + if (!cronAuth.ok) return cronAuth.response; + try { const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!; const SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; diff --git a/supabase/functions/process-queue/index.ts b/supabase/functions/process-queue/index.ts index 48219a4ac..868500c8f 100644 --- a/supabase/functions/process-queue/index.ts +++ b/supabase/functions/process-queue/index.ts @@ -1,9 +1,15 @@ import { createClient } from "https://esm.sh/@supabase/supabase-js@2.49.4"; import { buildPublicCorsHeaders } from "../_shared/cors.ts"; +import { authorizeCron } from "../_shared/dispatcher-auth.ts"; const corsHeaders = buildPublicCorsHeaders(); Deno.serve(async (req) => { + // Cron: exige x-cron-secret para evitar chamadas diretas não autorizadas + if (req.method === "OPTIONS") return new Response(null, { status: 204 }); + const cronAuth = authorizeCron(req, { corsHeaders: {}, secretEnvName: "CRON_SECRET", headerName: "x-cron-secret" }); + if (!cronAuth.ok) return cronAuth.response; + if (req.method === 'OPTIONS') { return new Response('ok', { headers: corsHeaders }); } diff --git a/supabase/functions/process-scheduled-reports/index.ts b/supabase/functions/process-scheduled-reports/index.ts index bec4db513..2f0dcb3f3 100644 --- a/supabase/functions/process-scheduled-reports/index.ts +++ b/supabase/functions/process-scheduled-reports/index.ts @@ -1,9 +1,15 @@ import { createClient } from "https://esm.sh/@supabase/supabase-js@2.49.4"; import { buildPublicCorsHeaders } from "../_shared/cors.ts"; +import { authorizeCron } from "../_shared/dispatcher-auth.ts"; const corsHeaders = buildPublicCorsHeaders(); Deno.serve(async (req) => { + // Cron: exige x-cron-secret para evitar chamadas diretas não autorizadas + if (req.method === "OPTIONS") return new Response(null, { status: 204 }); + const cronAuth = authorizeCron(req, { corsHeaders: {}, secretEnvName: "CRON_SECRET", headerName: "x-cron-secret" }); + if (!cronAuth.ok) return cronAuth.response; + if (req.method === "OPTIONS") { return new Response("ok", { headers: corsHeaders }); } diff --git a/supabase/functions/quote-followup-reminders/index.ts b/supabase/functions/quote-followup-reminders/index.ts index f3876fec5..959c79c2f 100644 --- a/supabase/functions/quote-followup-reminders/index.ts +++ b/supabase/functions/quote-followup-reminders/index.ts @@ -5,12 +5,21 @@ */ import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.0"; import { buildPublicCorsHeaders } from "../_shared/cors.ts"; +import { authorizeCron } from "../_shared/dispatcher-auth.ts"; const corsHeaders = buildPublicCorsHeaders(); Deno.serve(async (req) => { if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders }); + // Cron: exige x-cron-secret para evitar chamadas diretas não autorizadas + const cronAuth = authorizeCron(req, { + corsHeaders: {}, + secretEnvName: "CRON_SECRET", + headerName: "x-cron-secret", + }); + if (!cronAuth.ok) return cronAuth.response; + try { const supabase = createClient( Deno.env.get("SUPABASE_URL")!, diff --git a/supabase/functions/quote-sync/index.ts b/supabase/functions/quote-sync/index.ts index 405b295de..01c75e9c3 100644 --- a/supabase/functions/quote-sync/index.ts +++ b/supabase/functions/quote-sync/index.ts @@ -1,4 +1,5 @@ import { getCorsHeaders } from "../_shared/cors.ts"; +import { authenticateRequest, requireRole, authErrorResponse } from "../_shared/auth.ts"; /// import { createClient } from "npm:@supabase/supabase-js@2.49.1"; import { z } from "https://esm.sh/zod@3.23.8"; @@ -104,6 +105,14 @@ Deno.serve(async (req) => { } const corsHeaders = getCorsHeaders(req); + // Auth: exige vendedor autenticado (agente ou acima) + try { + const authCtx = await authenticateRequest(req); + requireRole(authCtx, "agente"); + } catch (authErr) { + return authErrorResponse(authErr, corsHeaders); + } + const supabase = createClient(supabaseUrl, supabaseServiceKey); diff --git a/supabase/functions/send-digest/index.ts b/supabase/functions/send-digest/index.ts index ed7982801..c8b5f3ddf 100644 --- a/supabase/functions/send-digest/index.ts +++ b/supabase/functions/send-digest/index.ts @@ -1,9 +1,15 @@ import { createClient } from "https://esm.sh/@supabase/supabase-js@2.49.4"; import { buildPublicCorsHeaders } from "../_shared/cors.ts"; +import { authorizeCron } from "../_shared/dispatcher-auth.ts"; const corsHeaders = buildPublicCorsHeaders(); Deno.serve(async (req) => { + // Cron: exige x-cron-secret para evitar chamadas diretas não autorizadas + if (req.method === "OPTIONS") return new Response(null, { status: 204 }); + const cronAuth = authorizeCron(req, { corsHeaders: {}, secretEnvName: "CRON_SECRET", headerName: "x-cron-secret" }); + if (!cronAuth.ok) return cronAuth.response; + if (req.method === 'OPTIONS') { return new Response('ok', { headers: corsHeaders }); } diff --git a/supabase/functions/send-notification/index.ts b/supabase/functions/send-notification/index.ts index 40a370e93..2a46122c5 100644 --- a/supabase/functions/send-notification/index.ts +++ b/supabase/functions/send-notification/index.ts @@ -2,6 +2,7 @@ import { getCorsHeaders } from '../_shared/cors.ts'; import { createClient } from "https://esm.sh/@supabase/supabase-js@2.49.4"; import { z } from "npm:zod@3.23.8"; import { castRpcResult } from "../_shared/supabase-client-adapter.ts"; +import { authorizeCron } from "../_shared/dispatcher-auth.ts"; const NotificationSchema = z.object({ user_id: z.string().uuid(), @@ -21,6 +22,11 @@ function jsonRes(corsHeaders: Record, body: unknown, status = 20 } Deno.serve(async (req) => { + // Cron: exige x-cron-secret para evitar chamadas diretas não autorizadas + if (req.method === "OPTIONS") return new Response(null, { status: 204 }); + const cronAuth = authorizeCron(req, { corsHeaders: {}, secretEnvName: "CRON_SECRET", headerName: "x-cron-secret" }); + if (!cronAuth.ok) return cronAuth.response; + const corsHeaders = getCorsHeaders(req); if (req.method === 'OPTIONS') { return new Response('ok', { headers: corsHeaders }); diff --git a/supabase/functions/send-scheduled-reports/index.ts b/supabase/functions/send-scheduled-reports/index.ts index b9324e65b..77d9d25b5 100644 --- a/supabase/functions/send-scheduled-reports/index.ts +++ b/supabase/functions/send-scheduled-reports/index.ts @@ -1,5 +1,6 @@ import { createClient } from "https://esm.sh/@supabase/supabase-js@2.49.4"; import { buildPublicCorsHeaders } from "../_shared/cors.ts"; +import { authorizeCron } from "../_shared/dispatcher-auth.ts"; const corsHeaders = buildPublicCorsHeaders();