- 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)
211 lines
6.7 KiB
TypeScript
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),
|
|
);
|
|
}
|
|
}
|