import { serve } from "https://deno.land/std@0.190.0/http/server.ts"; import { createClient } from "https://esm.sh/@supabase/supabase-js@2"; const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type", }; interface GoogleServiceAccount { type: string; project_id: string; private_key_id: string; private_key: string; client_email: string; client_id: string; token_uri: string; } interface CreateMeetRequest { slot_id: string; date: string; start_time: string; end_time: string; client_name: string; client_email: string; topic: string; notes?: string; } // Function to create JWT and get access token using native Web Crypto API async function getGoogleAccessToken(serviceAccount: GoogleServiceAccount): Promise { try { const now = Math.floor(Date.now() / 1000); // Build JWT header and payload manually const header = { alg: "RS256", typ: "JWT", }; const payload = { iss: serviceAccount.client_email, scope: "https://www.googleapis.com/auth/calendar", aud: serviceAccount.token_uri, exp: now + 3600, iat: now, }; // Encode header and payload (base64url) const base64UrlEncode = (str: string) => { return btoa(str) .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=/g, ""); }; const encodedHeader = base64UrlEncode(JSON.stringify(header)); const encodedPayload = base64UrlEncode(JSON.stringify(payload)); const signatureInput = `${encodedHeader}.${encodedPayload}`; // Convert PEM to binary const keyData = serviceAccount.private_key .replace(/-----BEGIN PRIVATE KEY-----/g, "") .replace(/-----END PRIVATE KEY-----/g, "") .replace(/\s/g, ""); const binaryKey = Uint8Array.from(atob(keyData), c => c.charCodeAt(0)); // Import private key using native Web Crypto API const privateKey = await crypto.subtle.importKey( "pkcs8", binaryKey, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256", }, false, ["sign"] ); // Sign the JWT const signature = await crypto.subtle.sign( "RSASSA-PKCS1-v1_5", privateKey, new TextEncoder().encode(signatureInput) ); // Encode signature const encodedSignature = btoa(String.fromCharCode(...new Uint8Array(signature))) .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=/g, ""); const token = `${signatureInput}.${encodedSignature}`; console.log("Generated JWT (first 100 chars):", token.substring(0, 100)); // Exchange JWT for access token const response = await fetch(serviceAccount.token_uri, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", assertion: token, }), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Token exchange failed: ${errorText}`); } const data = await response.json(); if (!data.access_token) { throw new Error("No access token in response"); } return data.access_token; } catch (error: any) { console.error("Error getting Google access token:", error); throw error; } } serve(async (req: Request): Promise => { if (req.method === "OPTIONS") { return new Response(null, { headers: corsHeaders }); } try { const supabaseUrl = Deno.env.get("SUPABASE_URL")!; const supabaseServiceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; const supabase = createClient(supabaseUrl, supabaseServiceKey); const body: CreateMeetRequest = await req.json(); console.log("Creating Google Meet event for slot:", body.slot_id); // Get platform settings const { data: settings, error: settingsError } = await supabase .from("platform_settings") .select("integration_google_calendar_id, google_service_account_json") .single(); if (settingsError) { console.error("Error fetching settings:", settingsError); throw settingsError; } const calendarId = settings?.integration_google_calendar_id; if (!calendarId) { return new Response( JSON.stringify({ success: false, message: "Google Calendar ID belum dikonfigurasi di Pengaturan > Integrasi" }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } // Get service account from settings const serviceAccountJson = settings?.google_service_account_json; if (!serviceAccountJson) { return new Response( JSON.stringify({ success: false, message: "Google Service Account JSON belum dikonfigurasi. Tambahkan di Pengaturan > Integrasi." }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } // Parse service account JSON let serviceAccount: GoogleServiceAccount; try { serviceAccount = JSON.parse(serviceAccountJson); } catch (error: any) { console.error("Failed to parse service account JSON:", error); return new Response( JSON.stringify({ success: false, message: "Format Google Service Account JSON tidak valid: " + error.message }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } // Get access token const accessToken = await getGoogleAccessToken(serviceAccount); console.log("Got access token"); // Build event data const startDate = new Date(`${body.date}T${body.start_time}`); const endDate = new Date(`${body.date}T${body.end_time}`); const eventData = { summary: `Konsultasi: ${body.topic} - ${body.client_name}`, description: `Client: ${body.client_email}\n\nNotes: ${body.notes || '-'}\n\nSlot ID: ${body.slot_id}`, start: { dateTime: startDate.toISOString(), timeZone: "Asia/Jakarta", }, end: { dateTime: endDate.toISOString(), timeZone: "Asia/Jakarta", }, // Note: attendees removed because service accounts need Domain-Wide Delegation // Client email is included in description instead conferenceData: { createRequest: { requestId: body.slot_id, }, }, }; console.log("Creating event in calendar:", calendarId); console.log("Event data:", JSON.stringify(eventData, null, 2)); // Create event via Google Calendar API const calendarResponse = await fetch( `https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(calendarId)}/events?conferenceDataVersion=1`, { method: "POST", headers: { "Authorization": `Bearer ${accessToken}`, "Content-Type": "application/json", }, body: JSON.stringify(eventData), } ); console.log("Calendar API response status:", calendarResponse.status); if (!calendarResponse.ok) { const errorText = await calendarResponse.text(); console.error("Google Calendar API error:", errorText); return new Response( JSON.stringify({ success: false, message: "Gagal membuat event di Google Calendar: " + errorText }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } const eventDataResult = await calendarResponse.json(); console.log("Event created:", eventDataResult.id); console.log("Full event response:", JSON.stringify(eventDataResult, null, 2)); // Check if conference data was created if (eventDataResult.conferenceData && eventDataResult.conferenceData.entryPoints) { const meetLink = eventDataResult.conferenceData.entryPoints.find((ep: any) => ep.entryPointType === "video")?.uri; if (meetLink) { await supabase .from("consulting_slots") .update({ meet_link: meetLink }) .eq("id", body.slot_id); return new Response( JSON.stringify({ success: true, meet_link: meetLink, event_id: eventDataResult.id, html_link: eventDataResult.htmlLink, }), { headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } } // Fallback to hangoutLink for backwards compatibility if (eventDataResult.hangoutLink) { await supabase .from("consulting_slots") .update({ meet_link: eventDataResult.hangoutLink }) .eq("id", body.slot_id); return new Response( JSON.stringify({ success: true, meet_link: eventDataResult.hangoutLink, event_id: eventDataResult.id, html_link: eventDataResult.htmlLink, }), { headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } return new Response( JSON.stringify({ success: false, message: "Event berhasil dibuat tapi tidak ada meet link" }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } catch (error: any) { console.error("Error creating Google Meet event:", error); return new Response( JSON.stringify({ success: false, message: error.message || "Unknown error occurred" }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } });