mirror of
https://github.com/domfelipe/mika-agent-assist.git
synced 2026-08-07 07:36:41 +00:00
Changes
Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com>
This commit is contained in:
parent
d18e1ad27c
commit
dd9f97cf79
6 changed files with 1177 additions and 0 deletions
152
supabase/functions/oauth-start/index.ts
Normal file
152
supabase/functions/oauth-start/index.ts
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
// 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,
|
||||
);
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue