mika-agent-assist/supabase/functions/oauth-start/index.ts
gpt-engineer-app[bot] dd9f97cf79 Changes
Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com>
2026-04-18 01:53:13 +00:00

152 lines
4.8 KiB
TypeScript

// oauth-start (autenticada via JWT)
// Recebe { mcp_slug }, valida plano e retorna { auth_url } para o frontend redirecionar.
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
import { corsHeaders } from "../_shared/cors.ts";
import {
buildAuthorizeUrl,
getProviderEnv,
type ProviderSlug,
} from "../_shared/oauth-providers.ts";
function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
function generateStateToken(): string {
const bytes = new Uint8Array(32);
crypto.getRandomValues(bytes);
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
}
Deno.serve(async (req) => {
if (req.method === "OPTIONS") {
return new Response(null, { headers: corsHeaders });
}
try {
const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
const serviceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
const anonKey = Deno.env.get("SUPABASE_ANON_KEY")!;
const authHeader = req.headers.get("Authorization") ?? "";
const userClient = createClient(supabaseUrl, anonKey, {
global: { headers: { Authorization: authHeader } },
});
const { data: userData, error: userErr } = await userClient.auth.getUser();
if (userErr || !userData.user) {
return jsonResponse({ error: "Não autenticado" }, 401);
}
const userId = userData.user.id;
const { mcp_slug } = await req.json() as { mcp_slug?: string };
if (!mcp_slug) {
return jsonResponse({ error: "mcp_slug é obrigatório" }, 400);
}
const admin = createClient(supabaseUrl, serviceKey);
// Busca MCP
const { data: mcp, error: mcpErr } = await admin
.from("available_mcps")
.select("id, slug, oauth_authorize_url, required_scopes, available_in_plans, is_active")
.eq("slug", mcp_slug)
.maybeSingle();
if (mcpErr || !mcp || !mcp.is_active) {
return jsonResponse({ error: "Integração não encontrada" }, 404);
}
// Valida agente ativo
const { data: agent } = await admin
.from("agent_instances")
.select("status")
.eq("user_id", userId)
.maybeSingle();
if (!agent || agent.status !== "active") {
return jsonResponse(
{ error: "agent_inactive", message: "Seu agente ainda não está ativo. Complete o onboarding." },
403,
);
}
// Valida plano
const { data: limits } = await admin
.from("user_integration_limits")
.select("plan_slug, max_integrations, current_integrations_count")
.eq("user_id", userId)
.maybeSingle();
if (!limits || !limits.plan_slug || !limits.max_integrations) {
return jsonResponse(
{ error: "no_subscription", message: "Você precisa de uma assinatura ativa." },
403,
);
}
const availableIn = (mcp.available_in_plans as string[]) ?? [];
if (!availableIn.includes(limits.plan_slug)) {
return jsonResponse(
{
error: "plan_not_allowed",
message: `A integração ${mcp.slug} não está disponível no plano ${limits.plan_slug}.`,
},
403,
);
}
// Verifica se já não atingiu limite (evita iniciar fluxo que vai falhar)
if (
(limits.current_integrations_count ?? 0) >= limits.max_integrations
) {
// Permite reconectar uma existente; mas se for nova vai falhar.
const { data: existing } = await admin
.from("user_integrations")
.select("id")
.eq("user_id", userId)
.eq("mcp_id", mcp.id)
.maybeSingle();
if (!existing) {
return jsonResponse(
{ error: "limit_reached", message: "Você atingiu o limite de integrações do seu plano." },
403,
);
}
}
// Gera state token
const stateToken = generateStateToken();
const { error: stErr } = await admin.from("oauth_state_tokens").insert({
state_token: stateToken,
user_id: userId,
mcp_id: mcp.id,
});
if (stErr) {
console.error("oauth_state insert error", stErr);
return jsonResponse({ error: "Falha ao iniciar OAuth" }, 500);
}
// Monta URL de autorização
const redirectUri = `${supabaseUrl}/functions/v1/oauth-callback`;
const env = getProviderEnv(mcp.slug as ProviderSlug, redirectUri);
const authUrl = buildAuthorizeUrl(
mcp.slug as ProviderSlug,
mcp.oauth_authorize_url,
(mcp.required_scopes as string[]) ?? [],
stateToken,
env,
);
return jsonResponse({ auth_url: authUrl });
} catch (err) {
console.error("oauth-start fatal", err instanceof Error ? err.message : "unknown");
return jsonResponse(
{ error: err instanceof Error ? err.message : "Erro inesperado" },
500,
);
}
});