From 929e6417929facca89cfa4bdbba1b3de0c6f7a62 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 21:01:21 +0000 Subject: [PATCH] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- .../mika/integrations/DisconnectMCPDialog.tsx | 14 ++++- src/routes/painel.integracoes.$slug.tsx | 25 ++++----- supabase/functions/_shared/oauth-providers.ts | 51 ++++++++++++++----- .../functions/disconnect-integration/index.ts | 9 ++-- 4 files changed, 68 insertions(+), 31 deletions(-) diff --git a/src/components/mika/integrations/DisconnectMCPDialog.tsx b/src/components/mika/integrations/DisconnectMCPDialog.tsx index 7551e16..94e8ad4 100644 --- a/src/components/mika/integrations/DisconnectMCPDialog.tsx +++ b/src/components/mika/integrations/DisconnectMCPDialog.tsx @@ -20,6 +20,7 @@ import { invokeFunction } from "@/lib/invoke-function"; interface Props { open: boolean; onOpenChange: (open: boolean) => void; + onDisconnected?: () => void; integrationId: string; mcpSlug: string; mcpName: string; @@ -28,6 +29,7 @@ interface Props { export function DisconnectMCPDialog({ open, onOpenChange, + onDisconnected, integrationId, mcpSlug, mcpName, @@ -49,6 +51,7 @@ export function DisconnectMCPDialog({ runtime_sync_warning?: string | null; }>("disconnect-integration", { integration_id: integrationId, + force_pause_jobs: dependentJobs.length > 0, }); setSubmitting(false); if (error) { @@ -56,14 +59,19 @@ export function DisconnectMCPDialog({ return; } toast.success(`${mcpName} desconectado.`); + if ((data?.paused_jobs_count ?? 0) > 0) { + toast.info(`${data!.paused_jobs_count} automação(ões) pausada(s).`); + } if (data?.runtime_sync_warning) { toast.warning("Integração removida, mas o runtime do agente não sincronizou."); } queryClient.invalidateQueries({ queryKey: ["user-integrations"] }); queryClient.invalidateQueries({ queryKey: ["user-integration-limits"] }); onOpenChange(false); + onDisconnected?.(); } + return ( @@ -101,7 +109,8 @@ export function DisconnectMCPDialog({ )}

- Pause ou exclua essas automações antes de desconectar. + Elas serão pausadas automaticamente ao desconectar. +

@@ -117,7 +126,8 @@ export function DisconnectMCPDialog({ e.preventDefault(); handleDisconnect(); }} - disabled={submitting || isLoading || hasActiveJobs} + disabled={submitting || isLoading} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" > {submitting ? "Desconectando..." : "Desconectar"} diff --git a/src/routes/painel.integracoes.$slug.tsx b/src/routes/painel.integracoes.$slug.tsx index cee8c57..2e05919 100644 --- a/src/routes/painel.integracoes.$slug.tsx +++ b/src/routes/painel.integracoes.$slug.tsx @@ -102,14 +102,19 @@ function IntegrationDetailPage() { async function handleTest() { setTesting(true); - const { data, error } = await invokeFunction<{ ok: boolean; account?: string }>( + const { data, error } = await invokeFunction<{ + ok?: boolean; + success?: boolean; + account?: string; + account_info?: unknown; + }>( "test-integration", { integration_id: integration!.id }, ); setTesting(false); if (error) { toast.error(error.message); - } else if (data?.ok) { + } else if (data?.ok || data?.success) { toast.success("Conexão funcionando perfeitamente."); } else { toast.warning("Conexão respondeu, mas com aviso. Verifique status."); @@ -117,6 +122,7 @@ function IntegrationDetailPage() { queryClient.invalidateQueries({ queryKey: ["user-integrations"] }); } + async function handleRefresh() { setRefreshing(true); const { data, error } = await invokeFunction<{ @@ -289,22 +295,13 @@ function IntegrationDetailPage() { { - setDisconnectOpen(o); - if (!o) { - // se desconectou, volta para a lista - queryClient.invalidateQueries({ queryKey: ["user-integrations"] }).then(() => { - const stillConnected = integs.some((i) => i.id === integration.id); - if (!stillConnected) { - navigate({ to: "/painel/integracoes", search: {} }); - } - }); - } - }} + onOpenChange={setDisconnectOpen} + onDisconnected={() => navigate({ to: "/painel/integracoes", search: {} })} integrationId={integration.id} mcpSlug={mcp.slug} mcpName={mcp.name} /> + ); } diff --git a/supabase/functions/_shared/oauth-providers.ts b/supabase/functions/_shared/oauth-providers.ts index 677adc9..bfb9085 100644 --- a/supabase/functions/_shared/oauth-providers.ts +++ b/supabase/functions/_shared/oauth-providers.ts @@ -23,23 +23,49 @@ export interface ProviderEnv { redirectUri: string; } +function readFirstEnv(keys: string[]): string { + for (const key of keys) { + const value = Deno.env.get(key); + if (value) return value; + } + return ""; +} + export function getProviderEnv(slug: ProviderSlug, redirectUri: string): ProviderEnv { - const map: Record = { - google_workspace: ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"], - notion: ["NOTION_CLIENT_ID", "NOTION_CLIENT_SECRET"], - todoist: ["TODOIST_CLIENT_ID", "TODOIST_CLIENT_SECRET"], - calcom: ["CALCOM_CLIENT_ID", "CALCOM_CLIENT_SECRET"], - microsoft_365: ["MICROSOFT_CLIENT_ID", "MICROSOFT_CLIENT_SECRET"], + const map: Record = { + google_workspace: { + id: ["GOOGLE_CLIENT_ID", "GOOGLE_OAUTH_CLIENT_ID"], + secret: ["GOOGLE_CLIENT_SECRET", "GOOGLE_OAUTH_CLIENT_SECRET"], + }, + notion: { + id: ["NOTION_CLIENT_ID", "NOTION_OAUTH_CLIENT_ID"], + secret: ["NOTION_CLIENT_SECRET", "NOTION_OAUTH_CLIENT_SECRET"], + }, + todoist: { + id: ["TODOIST_CLIENT_ID", "TODOIST_OAUTH_CLIENT_ID"], + secret: ["TODOIST_CLIENT_SECRET", "TODOIST_OAUTH_CLIENT_SECRET"], + }, + calcom: { + id: ["CALCOM_CLIENT_ID", "CALCOM_OAUTH_CLIENT_ID"], + secret: ["CALCOM_CLIENT_SECRET", "CALCOM_OAUTH_CLIENT_SECRET"], + }, + microsoft_365: { + id: ["MICROSOFT_CLIENT_ID", "MICROSOFT_OAUTH_CLIENT_ID"], + secret: ["MICROSOFT_CLIENT_SECRET", "MICROSOFT_OAUTH_CLIENT_SECRET"], + }, }; - const [idKey, secretKey] = map[slug]; - const clientId = Deno.env.get(idKey) ?? ""; - const clientSecret = Deno.env.get(secretKey) ?? ""; + const keys = map[slug]; + const clientId = readFirstEnv(keys.id); + const clientSecret = readFirstEnv(keys.secret); if (!clientId || !clientSecret) { - throw new Error(`Credenciais OAuth ausentes para ${slug} (${idKey}/${secretKey})`); + throw new Error( + `Credenciais OAuth ausentes para ${slug} (${keys.id.join(" ou ")}/${keys.secret.join(" ou ")})`, + ); } return { clientId, clientSecret, redirectUri }; } + /** * Monta a URL de autorização para iniciar o fluxo OAuth. */ @@ -415,8 +441,9 @@ export async function revokeToken( } case "todoist": { // Todoist precisa client_id/secret + access_token no body - const clientId = Deno.env.get("TODOIST_CLIENT_ID") ?? ""; - const clientSecret = Deno.env.get("TODOIST_CLIENT_SECRET") ?? ""; + const clientId = readFirstEnv(["TODOIST_CLIENT_ID", "TODOIST_OAUTH_CLIENT_ID"]); + const clientSecret = readFirstEnv(["TODOIST_CLIENT_SECRET", "TODOIST_OAUTH_CLIENT_SECRET"]); + const body = new URLSearchParams({ client_id: clientId, client_secret: clientSecret, diff --git a/supabase/functions/disconnect-integration/index.ts b/supabase/functions/disconnect-integration/index.ts index 7080753..f594f03 100644 --- a/supabase/functions/disconnect-integration/index.ts +++ b/supabase/functions/disconnect-integration/index.ts @@ -88,7 +88,7 @@ Deno.serve(async (req) => { // 1. Checa dependências const { data: dependentJobs } = await admin .from("scheduled_jobs") - .select("id, name") + .select("id, name, status") .eq("user_id", userId) .neq("status", "archived") .contains("required_mcp_slugs", [slug]); @@ -104,7 +104,8 @@ Deno.serve(async (req) => { } let pausedCount = 0; - if (dependentJobs && dependentJobs.length > 0 && force_pause_jobs) { + const jobsToPause = (dependentJobs ?? []).filter((job) => job.status === "active"); + if (jobsToPause.length > 0 && force_pause_jobs) { const { error: pErr } = await admin .from("scheduled_jobs") .update({ @@ -112,11 +113,13 @@ Deno.serve(async (req) => { auto_paused_reason: `Integração ${slug} foi desconectada`, }) .eq("user_id", userId) + .eq("status", "active") .neq("status", "archived") .contains("required_mcp_slugs", [slug]); - if (!pErr) pausedCount = dependentJobs.length; + if (!pErr) pausedCount = jobsToPause.length; } + // 2. Busca tokens ANTES de qualquer delete const accessToken = integ.access_token_vault_id ? await getDecryptedSecret(admin, integ.access_token_vault_id)