mirror of
https://github.com/domfelipe/mika-agent-assist.git
synced 2026-08-07 11:56:45 +00:00
Corrigiu hardening OAuth
X-Lovable-Edit-ID: edt-861b50d2-e4ce-4732-9e34-af896909cd73 Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com>
This commit is contained in:
commit
39e597906b
4 changed files with 68 additions and 31 deletions
|
|
@ -20,6 +20,7 @@ import { invokeFunction } from "@/lib/invoke-function";
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onDisconnected?: () => void;
|
||||||
integrationId: string;
|
integrationId: string;
|
||||||
mcpSlug: string;
|
mcpSlug: string;
|
||||||
mcpName: string;
|
mcpName: string;
|
||||||
|
|
@ -28,6 +29,7 @@ interface Props {
|
||||||
export function DisconnectMCPDialog({
|
export function DisconnectMCPDialog({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
|
onDisconnected,
|
||||||
integrationId,
|
integrationId,
|
||||||
mcpSlug,
|
mcpSlug,
|
||||||
mcpName,
|
mcpName,
|
||||||
|
|
@ -49,6 +51,7 @@ export function DisconnectMCPDialog({
|
||||||
runtime_sync_warning?: string | null;
|
runtime_sync_warning?: string | null;
|
||||||
}>("disconnect-integration", {
|
}>("disconnect-integration", {
|
||||||
integration_id: integrationId,
|
integration_id: integrationId,
|
||||||
|
force_pause_jobs: dependentJobs.length > 0,
|
||||||
});
|
});
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|
@ -56,14 +59,19 @@ export function DisconnectMCPDialog({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
toast.success(`${mcpName} desconectado.`);
|
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) {
|
if (data?.runtime_sync_warning) {
|
||||||
toast.warning("Integração removida, mas o runtime do agente não sincronizou.");
|
toast.warning("Integração removida, mas o runtime do agente não sincronizou.");
|
||||||
}
|
}
|
||||||
queryClient.invalidateQueries({ queryKey: ["user-integrations"] });
|
queryClient.invalidateQueries({ queryKey: ["user-integrations"] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["user-integration-limits"] });
|
queryClient.invalidateQueries({ queryKey: ["user-integration-limits"] });
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
|
onDisconnected?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
|
|
@ -101,7 +109,8 @@ export function DisconnectMCPDialog({
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
<p className="mt-2 text-xs">
|
<p className="mt-2 text-xs">
|
||||||
Pause ou exclua essas automações antes de desconectar.
|
Elas serão pausadas automaticamente ao desconectar.
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -117,7 +126,8 @@ export function DisconnectMCPDialog({
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleDisconnect();
|
handleDisconnect();
|
||||||
}}
|
}}
|
||||||
disabled={submitting || isLoading || hasActiveJobs}
|
disabled={submitting || isLoading}
|
||||||
|
|
||||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||||
>
|
>
|
||||||
{submitting ? "Desconectando..." : "Desconectar"}
|
{submitting ? "Desconectando..." : "Desconectar"}
|
||||||
|
|
|
||||||
|
|
@ -102,14 +102,19 @@ function IntegrationDetailPage() {
|
||||||
|
|
||||||
async function handleTest() {
|
async function handleTest() {
|
||||||
setTesting(true);
|
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",
|
"test-integration",
|
||||||
{ integration_id: integration!.id },
|
{ integration_id: integration!.id },
|
||||||
);
|
);
|
||||||
setTesting(false);
|
setTesting(false);
|
||||||
if (error) {
|
if (error) {
|
||||||
toast.error(error.message);
|
toast.error(error.message);
|
||||||
} else if (data?.ok) {
|
} else if (data?.ok || data?.success) {
|
||||||
toast.success("Conexão funcionando perfeitamente.");
|
toast.success("Conexão funcionando perfeitamente.");
|
||||||
} else {
|
} else {
|
||||||
toast.warning("Conexão respondeu, mas com aviso. Verifique status.");
|
toast.warning("Conexão respondeu, mas com aviso. Verifique status.");
|
||||||
|
|
@ -117,6 +122,7 @@ function IntegrationDetailPage() {
|
||||||
queryClient.invalidateQueries({ queryKey: ["user-integrations"] });
|
queryClient.invalidateQueries({ queryKey: ["user-integrations"] });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function handleRefresh() {
|
async function handleRefresh() {
|
||||||
setRefreshing(true);
|
setRefreshing(true);
|
||||||
const { data, error } = await invokeFunction<{
|
const { data, error } = await invokeFunction<{
|
||||||
|
|
@ -289,22 +295,13 @@ function IntegrationDetailPage() {
|
||||||
|
|
||||||
<DisconnectMCPDialog
|
<DisconnectMCPDialog
|
||||||
open={disconnectOpen}
|
open={disconnectOpen}
|
||||||
onOpenChange={(o) => {
|
onOpenChange={setDisconnectOpen}
|
||||||
setDisconnectOpen(o);
|
onDisconnected={() => navigate({ to: "/painel/integracoes", search: {} })}
|
||||||
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: {} });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
integrationId={integration.id}
|
integrationId={integration.id}
|
||||||
mcpSlug={mcp.slug}
|
mcpSlug={mcp.slug}
|
||||||
mcpName={mcp.name}
|
mcpName={mcp.name}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,23 +23,49 @@ export interface ProviderEnv {
|
||||||
redirectUri: string;
|
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 {
|
export function getProviderEnv(slug: ProviderSlug, redirectUri: string): ProviderEnv {
|
||||||
const map: Record<ProviderSlug, [string, string]> = {
|
const map: Record<ProviderSlug, { id: string[]; secret: string[] }> = {
|
||||||
google_workspace: ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"],
|
google_workspace: {
|
||||||
notion: ["NOTION_CLIENT_ID", "NOTION_CLIENT_SECRET"],
|
id: ["GOOGLE_CLIENT_ID", "GOOGLE_OAUTH_CLIENT_ID"],
|
||||||
todoist: ["TODOIST_CLIENT_ID", "TODOIST_CLIENT_SECRET"],
|
secret: ["GOOGLE_CLIENT_SECRET", "GOOGLE_OAUTH_CLIENT_SECRET"],
|
||||||
calcom: ["CALCOM_CLIENT_ID", "CALCOM_CLIENT_SECRET"],
|
},
|
||||||
microsoft_365: ["MICROSOFT_CLIENT_ID", "MICROSOFT_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 keys = map[slug];
|
||||||
const clientId = Deno.env.get(idKey) ?? "";
|
const clientId = readFirstEnv(keys.id);
|
||||||
const clientSecret = Deno.env.get(secretKey) ?? "";
|
const clientSecret = readFirstEnv(keys.secret);
|
||||||
if (!clientId || !clientSecret) {
|
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 };
|
return { clientId, clientSecret, redirectUri };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Monta a URL de autorização para iniciar o fluxo OAuth.
|
* Monta a URL de autorização para iniciar o fluxo OAuth.
|
||||||
*/
|
*/
|
||||||
|
|
@ -415,8 +441,9 @@ export async function revokeToken(
|
||||||
}
|
}
|
||||||
case "todoist": {
|
case "todoist": {
|
||||||
// Todoist precisa client_id/secret + access_token no body
|
// Todoist precisa client_id/secret + access_token no body
|
||||||
const clientId = Deno.env.get("TODOIST_CLIENT_ID") ?? "";
|
const clientId = readFirstEnv(["TODOIST_CLIENT_ID", "TODOIST_OAUTH_CLIENT_ID"]);
|
||||||
const clientSecret = Deno.env.get("TODOIST_CLIENT_SECRET") ?? "";
|
const clientSecret = readFirstEnv(["TODOIST_CLIENT_SECRET", "TODOIST_OAUTH_CLIENT_SECRET"]);
|
||||||
|
|
||||||
const body = new URLSearchParams({
|
const body = new URLSearchParams({
|
||||||
client_id: clientId,
|
client_id: clientId,
|
||||||
client_secret: clientSecret,
|
client_secret: clientSecret,
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ Deno.serve(async (req) => {
|
||||||
// 1. Checa dependências
|
// 1. Checa dependências
|
||||||
const { data: dependentJobs } = await admin
|
const { data: dependentJobs } = await admin
|
||||||
.from("scheduled_jobs")
|
.from("scheduled_jobs")
|
||||||
.select("id, name")
|
.select("id, name, status")
|
||||||
.eq("user_id", userId)
|
.eq("user_id", userId)
|
||||||
.neq("status", "archived")
|
.neq("status", "archived")
|
||||||
.contains("required_mcp_slugs", [slug]);
|
.contains("required_mcp_slugs", [slug]);
|
||||||
|
|
@ -104,7 +104,8 @@ Deno.serve(async (req) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
let pausedCount = 0;
|
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
|
const { error: pErr } = await admin
|
||||||
.from("scheduled_jobs")
|
.from("scheduled_jobs")
|
||||||
.update({
|
.update({
|
||||||
|
|
@ -112,11 +113,13 @@ Deno.serve(async (req) => {
|
||||||
auto_paused_reason: `Integração ${slug} foi desconectada`,
|
auto_paused_reason: `Integração ${slug} foi desconectada`,
|
||||||
})
|
})
|
||||||
.eq("user_id", userId)
|
.eq("user_id", userId)
|
||||||
|
.eq("status", "active")
|
||||||
.neq("status", "archived")
|
.neq("status", "archived")
|
||||||
.contains("required_mcp_slugs", [slug]);
|
.contains("required_mcp_slugs", [slug]);
|
||||||
if (!pErr) pausedCount = dependentJobs.length;
|
if (!pErr) pausedCount = jobsToPause.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 2. Busca tokens ANTES de qualquer delete
|
// 2. Busca tokens ANTES de qualquer delete
|
||||||
const accessToken = integ.access_token_vault_id
|
const accessToken = integ.access_token_vault_id
|
||||||
? await getDecryptedSecret(admin, integ.access_token_vault_id)
|
? await getDecryptedSecret(admin, integ.access_token_vault_id)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue