From 6e411b160a309fc757adea242e21cba957bfedb7 Mon Sep 17 00:00:00 2001 From: dwindown Date: Tue, 23 Dec 2025 09:58:12 +0700 Subject: [PATCH] Fix edge function and add to deployment script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update create-google-meet-event with improved JWT handling - Fix jose library import and token signing - Add better error logging - Include create-google-meet-event in deployment script 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- add-google-service-account-column.sql | 6 + deploy-edge-functions.sh | 1 + refresh-schema.sql | 5 + .../create-google-meet-event/index.ts | 129 ++++++++++-------- 4 files changed, 83 insertions(+), 58 deletions(-) create mode 100644 add-google-service-account-column.sql create mode 100644 refresh-schema.sql diff --git a/add-google-service-account-column.sql b/add-google-service-account-column.sql new file mode 100644 index 0000000..f008bf4 --- /dev/null +++ b/add-google-service-account-column.sql @@ -0,0 +1,6 @@ +-- Add google_service_account_json column to platform_settings +ALTER TABLE platform_settings +ADD COLUMN IF NOT EXISTS google_service_account_json TEXT; + +-- Add comment for documentation +COMMENT ON COLUMN platform_settings.google_service_account_json IS 'Google Service Account JSON for Calendar API integration (use service account to avoid OAuth)'; diff --git a/deploy-edge-functions.sh b/deploy-edge-functions.sh index b34fa71..3d308a7 100755 --- a/deploy-edge-functions.sh +++ b/deploy-edge-functions.sh @@ -33,6 +33,7 @@ deploy_function() { deploy_function "pakasir-webhook" deploy_function "send-test-email" deploy_function "create-meet-link" # Includes n8n test mode toggle +deploy_function "create-google-meet-event" # Direct Google Calendar API integration deploy_function "send-consultation-reminder" deploy_function "send-notification" deploy_function "send-email-v2" diff --git a/refresh-schema.sql b/refresh-schema.sql new file mode 100644 index 0000000..73d3fbe --- /dev/null +++ b/refresh-schema.sql @@ -0,0 +1,5 @@ +-- This query will force a schema refresh +SELECT column_name, data_type +FROM information_schema.columns +WHERE table_name = 'platform_settings' +AND column_name = 'google_service_account_json'; diff --git a/supabase/functions/create-google-meet-event/index.ts b/supabase/functions/create-google-meet-event/index.ts index ff60daa..37dcfd0 100644 --- a/supabase/functions/create-google-meet-event/index.ts +++ b/supabase/functions/create-google-meet-event/index.ts @@ -13,7 +13,6 @@ interface GoogleServiceAccount { private_key: string; client_email: string; client_id: string; - auth_uri: string; token_uri: string; } @@ -28,52 +27,65 @@ interface CreateMeetRequest { notes?: string; } -// Function to create JWT for Google API authentication +// Function to create JWT and get access token async function getGoogleAccessToken(serviceAccount: GoogleServiceAccount): Promise { - const header = { - alg: "RS256", - typ: "JWT", - }; + try { + // Import JWT library + const { jwt } = await import("https://deno.land/x/jose@v4.15.1/index.ts"); - const now = Math.floor(Date.now() / 1000); - const payload = { - iss: serviceAccount.client_email, - scope: "https://www.googleapis.com/auth/calendar", - aud: serviceAccount.token_uri, - exp: now + 3600, - iat: now, - }; + // Create JWT header and payload + const header = { + alg: "RS256", + typ: "JWT", + kid: serviceAccount.private_key_id, + }; - // Import JWT functionality - const { default: jwt } = await import("https://deno.land/x/jose@v4.14.4/index.ts"); + const now = Math.floor(Date.now() / 1000); + const payload = { + iss: serviceAccount.client_email, + scope: "https://www.googleapis.com/auth/calendar", + aud: serviceAccount.token_uri, + exp: now + 3600, + iat: now, + }; - const privateKey = await crypto.subtle.importKey( - "pkcs8", - StringEncoder.encode(serviceAccount.private_key), - { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, - false, - ["sign"] - ); + // Import key + const privateKey = serviceAccount.private_key; - const token = await jwt.sign(header, payload, privateKey); + // Sign JWT + const token = await jwt.sign(payload, privateKey, { + algorithm: 'RS256', + header: header, + }); - // 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, - }), - }); + // 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, + }), + }); - const data = await response.json(); - return data.access_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; + } } -// String encoder helper -const StringEncoder = new TextEncoder(); - serve(async (req: Request): Promise => { if (req.method === "OPTIONS") { return new Response(null, { headers: corsHeaders }); @@ -88,13 +100,17 @@ serve(async (req: Request): Promise => { console.log("Creating Google Meet event for slot:", body.slot_id); // Get platform settings - const { data: settings } = await supabase + const { data: settings, error: settingsError } = await supabase .from("platform_settings") - .select("integration_google_calendar_id, brand_name, google_service_account_json") + .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; - const brandName = settings?.brand_name || "LearnHub"; if (!calendarId) { return new Response( @@ -106,24 +122,14 @@ serve(async (req: Request): Promise => { ); } - // Get service account from settings or environment - let serviceAccountJson: string | null = null; - - // Priority 1: Check if stored in platform_settings (encrypted field recommended) - if (settings?.google_service_account_json) { - serviceAccountJson = settings.google_service_account_json; - } - - // Priority 2: Check environment variable - if (!serviceAccountJson) { - serviceAccountJson = Deno.env.get("GOOGLE_SERVICE_ACCOUNT_JSON") || null; - } + // 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 environment variables atau platform_settings." + message: "Google Service Account JSON belum dikonfigurasi. Tambahkan di Pengaturan > Integrasi." }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); @@ -133,12 +139,12 @@ serve(async (req: Request): Promise => { let serviceAccount: GoogleServiceAccount; try { serviceAccount = JSON.parse(serviceAccountJson); - } catch (error) { + } 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" + message: "Format Google Service Account JSON tidak valid: " + error.message }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); @@ -146,6 +152,7 @@ serve(async (req: Request): Promise => { // 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}`); @@ -153,7 +160,7 @@ serve(async (req: Request): Promise => { const eventData = { summary: `Konsultasi: ${body.topic} - ${body.client_name}`, - description: `Client: ${body.client_email}\n\nNotes: ${body.notes || '-'}\n\nSlot ID: ${body.slot_id}\nBrand: ${brandName}`, + description: `Client: ${body.client_email}\n\nNotes: ${body.notes || '-'}\n\nSlot ID: ${body.slot_id}`, start: { dateTime: startDate.toISOString(), timeZone: "Asia/Jakarta", @@ -177,6 +184,8 @@ serve(async (req: Request): Promise => { guestsCanSeeOtherGuests: false, }; + console.log("Creating event in calendar:", calendarId); + // Create event via Google Calendar API const calendarResponse = await fetch( `https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(calendarId)}/events?conferenceDataVersion=1`, @@ -203,6 +212,7 @@ serve(async (req: Request): Promise => { } const eventDataResult = await calendarResponse.json(); + console.log("Event created:", eventDataResult.id); // Update the slot with the meet link if (eventDataResult.hangoutLink) { @@ -233,7 +243,10 @@ serve(async (req: Request): Promise => { } catch (error: any) { console.error("Error creating Google Meet event:", error); return new Response( - JSON.stringify({ success: false, message: error.message }), + JSON.stringify({ + success: false, + message: error.message || "Unknown error occurred" + }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); }