Dashboard-Automatizase/app/api/google-calendar/callback/route.ts
Luis 1391fe6216 feat: enhance Google Calendar integration and update Dockerfile for environment variables
- Updated Dockerfile to include hardcoded environment variables for Next.js build.
- Enhanced Google Calendar API integration by extracting user email from id_token and adding scopes for OpenID and email access.
- Modified credential management to delete existing credentials before creating new ones in n8n.
- Updated dashboard to display connected Google Calendar email and credential details.

Story: 4.2 - Melhorar integração com Google Calendar e atualizar Dockerfile

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2025-10-12 21:16:21 -03:00

211 lines
6.7 KiB
TypeScript

import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
import { type NextRequest, NextResponse } from "next/server";
/**
* GET /api/google-calendar/callback
*
* Story 3.4: Intercepta callback do Google OAuth, troca code por tokens,
* e cria/atualiza credencial no n8n via API REST (gerenciamento programático).
*
* Fluxo:
* 1. Recebe code + state do Google
* 2. Troca code por access_token + refresh_token
* 3. Chama manage-credential para criar/atualizar credencial no n8n
* 4. Redireciona para dashboard com status
*/
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const code = searchParams.get("code");
const state = searchParams.get("state");
const error = searchParams.get("error");
// Se houver erro no OAuth, redirecionar para dashboard com erro
if (error) {
console.error("[callback] Erro no OAuth do Google:", error);
return NextResponse.redirect(
new URL(`/dashboard?oauth_error=${error}`, request.url),
);
}
// Validar que recebemos o código
if (!code) {
console.error("[callback] Callback sem code");
return NextResponse.redirect(
new URL("/dashboard?oauth_error=missing_code", request.url),
);
}
// Validar state para prevenir CSRF (SEC-001)
if (!state) {
console.error("[callback] Callback sem state (CSRF protection)");
return NextResponse.redirect(
new URL("/dashboard?oauth_error=invalid_state", request.url),
);
}
// Criar cliente Supabase com cookies para acessar sessão do usuário
const cookieStore = await cookies();
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll();
},
setAll(cookiesToSet) {
for (const { name, value, options } of cookiesToSet) {
cookieStore.set(name, value, options);
}
},
},
},
);
// Pegar user_id da sessão do Supabase
const {
data: { user },
} = await supabase.auth.getUser();
if (!user?.id) {
console.error("[callback] Usuário não autenticado");
return NextResponse.redirect(
new URL("/dashboard?oauth_error=unauthorized", request.url),
);
}
// Validar state contra user_id da sessão (CSRF protection - SEC-001)
if (state !== user.id) {
console.error("[callback] State mismatch - possível ataque CSRF:", {
expected: user.id,
received: state,
});
return NextResponse.redirect(
new URL("/dashboard?oauth_error=csrf_detected", request.url),
);
}
console.log("[callback] User:", user.id, "- State validated");
// Trocar code por tokens no Google
const tokenResponse = await fetch("https://oauth2.googleapis.com/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
code,
client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || "",
client_secret: process.env.GOOGLE_CLIENT_SECRET || "",
redirect_uri: `${process.env.NEXT_PUBLIC_SITE_URL}/api/google-calendar/callback`,
grant_type: "authorization_code",
}),
});
if (!tokenResponse.ok) {
const errorData = await tokenResponse.text();
console.error("[callback] Erro ao trocar code por tokens:", errorData);
return NextResponse.redirect(
new URL("/dashboard?oauth_error=token_exchange_failed", request.url),
);
}
const tokens = await tokenResponse.json();
console.log("[callback] Tokens obtidos do Google");
// Decodificar id_token para extrair email do usuário (JWT base64)
// O id_token é um JWT com 3 partes: header.payload.signature
let googleEmail: string | null = null;
if (tokens.id_token) {
try {
// Extrair payload (segunda parte do JWT)
const payloadBase64 = tokens.id_token.split(".")[1];
const payloadJson = Buffer.from(payloadBase64, "base64").toString(
"utf-8",
);
const payload = JSON.parse(payloadJson);
googleEmail = payload.email || null;
console.log("[callback] Email extraído do id_token:", googleEmail);
} catch (error) {
console.error("[callback] Erro ao decodificar id_token:", error);
// Se falhar, tentaremos buscar via userinfo API abaixo
}
}
// Fallback: Se não conseguimos extrair email do id_token, buscar via API
if (!googleEmail && tokens.access_token) {
try {
const userinfoResponse = await fetch(
"https://www.googleapis.com/oauth2/v1/userinfo?alt=json",
{
headers: {
Authorization: `Bearer ${tokens.access_token}`,
},
},
);
if (userinfoResponse.ok) {
const userinfo = await userinfoResponse.json();
googleEmail = userinfo.email || null;
console.log("[callback] Email obtido via userinfo API:", googleEmail);
}
} catch (error) {
console.error("[callback] Erro ao buscar userinfo:", error);
}
}
// Chamar manage-credential para criar/atualizar credencial no n8n
const manageCredentialUrl = new URL(
"/api/google-calendar/manage-credential",
request.url,
);
const manageResponse = await fetch(manageCredentialUrl.toString(), {
method: "POST",
headers: {
"Content-Type": "application/json",
Cookie: cookieStore
.getAll()
.map((c) => `${c.name}=${c.value}`)
.join("; "),
},
body: JSON.stringify({
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresIn: tokens.expires_in || 3599,
googleEmail: googleEmail, // Enviar email do Google
}),
});
if (!manageResponse.ok) {
const errorData = await manageResponse.text();
console.error(
"[callback] Erro ao gerenciar credencial no n8n:",
errorData,
);
return NextResponse.redirect(
new URL("/dashboard?oauth_error=n8n_credential_failed", request.url),
);
}
const manageResult = await manageResponse.json();
console.log(
"[callback] Credencial gerenciada no n8n:",
manageResult.credentialId,
);
// Redirecionar para dashboard com sucesso
return NextResponse.redirect(
new URL("/dashboard?oauth_success=true", request.url),
);
} catch (error) {
console.error("[callback] Erro ao processar callback:", error);
return NextResponse.redirect(
new URL("/dashboard?oauth_error=internal_error", request.url),
);
}
}