fix: harden integration oauth flow

This commit is contained in:
Felipe Domingues 2026-05-28 17:54:55 -03:00
parent 9acb125890
commit 354ca092c9
4 changed files with 60 additions and 31 deletions

View file

@ -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,12 +59,16 @@ 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 +108,7 @@ export function DisconnectMCPDialog({
)}
</ul>
<p className="mt-2 text-xs">
Pause ou exclua essas automações antes de desconectar.
Elas serão pausadas automaticamente ao desconectar.
</p>
</div>
</div>
@ -117,7 +124,7 @@ 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"}

View file

@ -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.");
@ -289,18 +294,8 @@ function IntegrationDetailPage() {
<DisconnectMCPDialog
open={disconnectOpen}
onOpenChange={(o) => {
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}

View file

@ -23,19 +23,44 @@ 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<ProviderSlug, [string, string]> = {
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<ProviderSlug, { id: string[]; secret: string[] }> = {
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 };
}
@ -415,8 +440,8 @@ 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,

View file

@ -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,9 +113,10 @@ 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