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), ); } }