mirror of
https://github.com/domfelipe/mika-agent-assist.git
synced 2026-08-07 12:56:51 +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 {
|
||||
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 (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent>
|
||||
|
|
@ -101,7 +109,8 @@ 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 +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"}
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
|
||||
<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}
|
||||
/>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<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 };
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue