Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot] 2026-05-23 17:52:46 +00:00
parent 1a1372e9ce
commit 4a331d6a69
2 changed files with 224 additions and 0 deletions

View file

@ -0,0 +1,128 @@
// internal-auth.ts
// Helper compartilhado para validar chamadas internas a edge functions.
// Permite que uma function aceite:
// - X-Internal-Secret válido (chamadas server-to-server, triggers pg_net, cron)
// - JWT de admin (painel admin via supabase.functions.invoke)
// - opcionalmente, JWT do dono do recurso (ownerUserId)
//
// Modo de operação:
// - Se INTERNAL_FUNCTION_SECRET estiver definido E o header X-Internal-Secret bater → OK
// - Senão, tenta validar o JWT do Authorization header
// - Retorna { ok, userId, isAdmin, isOwner, reason }
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.4";
export interface InternalAuthResult {
ok: boolean;
userId: string | null;
isAdmin: boolean;
isOwner: boolean;
viaSecret: boolean;
reason?: string;
}
export interface AuthorizeInternalOptions {
/** Se fornecido, valida ownership contra esse user_id. */
ownerUserId?: string | null;
/** Se true (default), permite admins. */
allowAdmin?: boolean;
/** Se true (default), permite o dono. */
allowOwner?: boolean;
/** Se true (default), permite via X-Internal-Secret. */
allowSecret?: boolean;
}
export async function authorizeInternalRequest(
req: Request,
opts: AuthorizeInternalOptions = {},
): Promise<InternalAuthResult> {
const {
ownerUserId = null,
allowAdmin = true,
allowOwner = true,
allowSecret = true,
} = opts;
// 1) X-Internal-Secret
if (allowSecret) {
const expected = Deno.env.get("INTERNAL_FUNCTION_SECRET") ?? "";
const received = req.headers.get("x-internal-secret") ?? "";
if (expected && received && constantTimeEq(expected, received)) {
return {
ok: true,
userId: null,
isAdmin: false,
isOwner: false,
viaSecret: true,
};
}
}
// 2) JWT
const authHeader = req.headers.get("authorization") ?? "";
if (!authHeader.toLowerCase().startsWith("bearer ")) {
return {
ok: false,
userId: null,
isAdmin: false,
isOwner: false,
viaSecret: false,
reason: "missing bearer token",
};
}
const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
const serviceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
const anonKey = Deno.env.get("SUPABASE_ANON_KEY") ?? Deno.env.get("SUPABASE_PUBLISHABLE_KEY") ?? "";
const userClient = createClient(supabaseUrl, anonKey, {
global: { headers: { Authorization: authHeader } },
auth: { persistSession: false, autoRefreshToken: false },
});
const { data: userData, error: userErr } = await userClient.auth.getUser();
if (userErr || !userData?.user) {
return {
ok: false,
userId: null,
isAdmin: false,
isOwner: false,
viaSecret: false,
reason: "invalid jwt",
};
}
const userId = userData.user.id;
let isAdmin = false;
if (allowAdmin) {
const admin = createClient(supabaseUrl, serviceKey, {
auth: { persistSession: false, autoRefreshToken: false },
});
const { data: roleOk } = await admin.rpc("has_role", {
_user_id: userId,
_role: "admin",
});
isAdmin = roleOk === true;
}
const isOwner = allowOwner && ownerUserId !== null && userId === ownerUserId;
if (isAdmin || isOwner) {
return { ok: true, userId, isAdmin, isOwner, viaSecret: false };
}
return {
ok: false,
userId,
isAdmin,
isOwner,
viaSecret: false,
reason: "forbidden",
};
}
function constantTimeEq(a: string, b: string): boolean {
if (a.length !== b.length) return false;
let diff = 0;
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
return diff === 0;
}

View file

@ -0,0 +1,96 @@
// bootstrap-internal-secret
// Função one-shot: lê INTERNAL_FUNCTION_SECRET de env e persiste em vault como
// 'internal_function_secret', para que triggers pg_net e cron job possam ler e
// enviar como X-Internal-Secret nas chamadas a outras edge functions.
//
// Requer JWT de admin. Idempotente — pode ser chamada várias vezes.
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.4";
import { corsHeaders } from "../_shared/cors.ts";
const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!;
const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
const SUPABASE_ANON_KEY = Deno.env.get("SUPABASE_ANON_KEY") ?? Deno.env.get("SUPABASE_PUBLISHABLE_KEY") ?? "";
function json(status: number, body: unknown) {
return new Response(JSON.stringify(body), {
status,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
Deno.serve(async (req) => {
if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders });
const secretValue = Deno.env.get("INTERNAL_FUNCTION_SECRET") ?? "";
if (!secretValue) {
return json(500, { error: "INTERNAL_FUNCTION_SECRET not set in env" });
}
// Validar admin via JWT
const authHeader = req.headers.get("authorization") ?? "";
if (!authHeader.toLowerCase().startsWith("bearer ")) {
return json(401, { error: "missing bearer token" });
}
const userClient = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
global: { headers: { Authorization: authHeader } },
auth: { persistSession: false, autoRefreshToken: false },
});
const { data: userData, error: userErr } = await userClient.auth.getUser();
if (userErr || !userData?.user) return json(401, { error: "invalid jwt" });
const admin = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, {
auth: { persistSession: false, autoRefreshToken: false },
});
const { data: roleOk } = await admin.rpc("has_role", {
_user_id: userData.user.id,
_role: "admin",
});
if (!roleOk) return json(403, { error: "admin role required" });
// Upsert no vault. Como vault.create_secret falha em nomes duplicados,
// tentamos atualizar existente primeiro via SQL direto.
try {
// 1) Tenta achar entry existente
const { data: existing } = await admin
.from("vault.decrypted_secrets" as never)
.select("id, decrypted_secret")
.eq("name", "internal_function_secret")
.maybeSingle()
.returns<{ id: string; decrypted_secret: string } | null>();
if (existing && existing.decrypted_secret === secretValue) {
return json(200, { ok: true, action: "already_synced" });
}
if (existing) {
// Atualiza o valor via RPC dedicada (não temos UPDATE direto em vault.secrets via PostgREST,
// então deletamos e recriamos).
await admin.rpc("vault_delete_secret", { secret_id: existing.id });
}
const { data: created, error: createErr } = await admin
.rpc("vault_create_secret", {
secret_value: secretValue,
secret_name: "internal_function_secret",
secret_description: "Shared secret for trigger pg_net → edge functions auth",
})
.single();
if (createErr) {
console.error("vault_create_secret failed:", createErr);
return json(500, { error: "vault create failed", detail: createErr.message });
}
return json(200, {
ok: true,
action: existing ? "rotated" : "created",
// deno-lint-ignore no-explicit-any
secret_id: (created as any)?.secret_id ?? created,
});
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
console.error("bootstrap-internal-secret fatal:", msg);
return json(500, { error: msg });
}
});