import { createClient } from "https://esm.sh/@supabase/supabase-js@2.49.1"; const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type, x-supabase-client-platform, x-supabase-client-platform-version, x-supabase-client-runtime, x-supabase-client-runtime-version", }; function jsonResponse(payload: unknown, status = 200) { return new Response(JSON.stringify(payload), { status, headers: { ...corsHeaders, "Content-Type": "application/json" }, }); } function extractUserIdFromAuthHeader(authHeader: string | null) { if (!authHeader?.startsWith("Bearer ")) return null; try { const token = authHeader.replace("Bearer ", ""); const payload = token.split(".")[1]; if (!payload) return null; const normalized = payload.replace(/-/g, "+").replace(/_/g, "/"); const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "="); const decoded = JSON.parse(atob(padded)); return typeof decoded?.sub === "string" ? decoded.sub : null; } catch (error) { console.error("google-drive-proxy token decode failed", error); return null; } } function mapGoogleDriveError(error: any) { const reason = error?.details?.find?.((detail: any) => detail?.reason)?.reason ?? error?.errors?.[0]?.reason; const message = error?.message || "Google Drive request failed."; if (/oauth client was not found|invalid_client/i.test(message) || reason === "invalid_client") { return { error: "Google Drive OAuth credentials are invalid or missing. Update the Google client ID/secret in the backend, then reconnect Google Drive.", code: "OAUTH_CLIENT_NOT_FOUND", reconnectRequired: true, }; } if (reason === "ACCESS_TOKEN_SCOPE_INSUFFICIENT" || reason === "insufficientPermissions") { return { error: "Your Google connection needs to be refreshed for Drive access. Disconnect Google and connect it again.", code: "RECONNECT_REQUIRED", reconnectRequired: true, }; } if (reason === "SERVICE_DISABLED" || /google drive api has not been used|drive\.googleapis\.com|api.*disabled/i.test(message)) { return { error: "Google Drive API is not enabled in the connected Google project.", code: "API_DISABLED", }; } return { error: message, code: "GOOGLE_API_ERROR", }; } async function getValidAccessToken( serviceClient: any, userId: string, clientId: string, clientSecret: string, ): Promise { const { data: tokenRow, error } = await serviceClient .from("google_drive_tokens") .select("*") .eq("user_id", userId) .single(); if (error || !tokenRow) throw new Error("NOT_CONNECTED"); const expiresAt = new Date(tokenRow.token_expires_at).getTime(); if (Date.now() < expiresAt - 60000) { return tokenRow.access_token; } const refreshRes = await fetch("https://oauth2.googleapis.com/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ client_id: clientId, client_secret: clientSecret, refresh_token: tokenRow.refresh_token, grant_type: "refresh_token", }), }); const refreshData = await refreshRes.json(); if (refreshData.error) throw new Error(refreshData.error_description || refreshData.error || "REFRESH_FAILED"); const newExpiresAt = new Date(Date.now() + refreshData.expires_in * 1000).toISOString(); await serviceClient .from("google_drive_tokens") .update({ access_token: refreshData.access_token, token_expires_at: newExpiresAt, }) .eq("user_id", userId); return refreshData.access_token; } Deno.serve(async (req) => { if (req.method === "OPTIONS") { return new Response(null, { headers: corsHeaders }); } try { const GOOGLE_CLIENT_ID = Deno.env.get("GOOGLE_CLIENT_ID")!; const GOOGLE_CLIENT_SECRET = Deno.env.get("GOOGLE_CLIENT_SECRET")!; const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!; const SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; if (!GOOGLE_CLIENT_ID || !GOOGLE_CLIENT_SECRET) { return jsonResponse({ error: "Google Drive OAuth credentials are not configured in the backend.", code: "OAUTH_CREDENTIALS_MISSING", reconnectRequired: true, }); } const authHeader = req.headers.get("Authorization"); const userId = extractUserIdFromAuthHeader(authHeader); if (!userId) { return jsonResponse({ error: "Unauthorized" }, 401); } const serviceClient = createClient(SUPABASE_URL, SERVICE_ROLE_KEY); const { data: roles } = await serviceClient .from("user_roles") .select("role") .eq("user_id", userId); const isAdmin = roles?.some((role: any) => role.role === "admin"); const body = await req.json(); const { action, folder_id, file_id } = body; if (action === "list_shared_drives") { if (!isAdmin) { return jsonResponse({ error: "Only admins can browse Drive" }, 403); } const accessToken = await getValidAccessToken(serviceClient, userId, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET); const drivesRes = await fetch( `https://www.googleapis.com/drive/v3/drives?pageSize=100`, { headers: { Authorization: `Bearer ${accessToken}` } }, ); const drivesData = await drivesRes.json(); if (drivesData.error) { return jsonResponse(mapGoogleDriveError(drivesData.error)); } return jsonResponse(drivesData); } if (action === "list_files") { if (!isAdmin) { return jsonResponse({ error: "Only admins can browse Drive" }, 403); } const accessToken = await getValidAccessToken(serviceClient, userId, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET); const parentId = folder_id || "root"; const query = `'${parentId}' in parents and trashed = false`; const fields = "files(id,name,mimeType,iconLink,webViewLink,size,modifiedTime),nextPageToken"; const params = new URLSearchParams({ q: query, fields, orderBy: "folder,name", pageSize: "100", supportsAllDrives: "true", includeItemsFromAllDrives: "true", corpora: "allDrives", }); const driveRes = await fetch( `https://www.googleapis.com/drive/v3/files?${params}`, { headers: { Authorization: `Bearer ${accessToken}` } }, ); const driveData = await driveRes.json(); if (driveData.error) { return jsonResponse(mapGoogleDriveError(driveData.error)); } return jsonResponse(driveData); } if (action === "download_file") { if (!file_id) { return jsonResponse({ error: "Missing file_id" }, 400); } let tokenUserId = userId; if (!isAdmin) { const { data: sharedFile } = await serviceClient .from("shared_drive_files") .select("shared_by") .eq("drive_file_id", file_id) .maybeSingle(); if (!sharedFile?.shared_by) { return jsonResponse({ error: "File not found or not shared" }, 404); } tokenUserId = sharedFile.shared_by; } const accessToken = await getValidAccessToken(serviceClient, tokenUserId, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET); const metaRes = await fetch( `https://www.googleapis.com/drive/v3/files/${file_id}?fields=name,mimeType&supportsAllDrives=true`, { headers: { Authorization: `Bearer ${accessToken}` } }, ); const meta = await metaRes.json(); if (meta?.error) { return jsonResponse(mapGoogleDriveError(meta.error)); } const downloadRes = await fetch( `https://www.googleapis.com/drive/v3/files/${file_id}?alt=media&supportsAllDrives=true`, { headers: { Authorization: `Bearer ${accessToken}` } }, ); if (!downloadRes.ok) { const errText = await downloadRes.text(); try { const parsed = JSON.parse(errText); if (parsed?.error) { return jsonResponse(mapGoogleDriveError(parsed.error)); } } catch { // fall through to generic error response } return jsonResponse({ error: `Download failed: ${errText}` }); } const fileBlob = await downloadRes.blob(); return new Response(fileBlob, { headers: { ...corsHeaders, "Content-Type": meta.mimeType || "application/octet-stream", "Content-Disposition": `attachment; filename="${meta.name}"`, }, }); } if (action === "upload_file") { if (!isAdmin) { return jsonResponse({ error: "Only admins can upload to Drive" }, 403); } const { file_name, file_base64, mime_type, parent_folder_id } = body; if (!file_name || !file_base64) { return jsonResponse({ error: "Missing file_name or file_base64" }, 400); } const accessToken = await getValidAccessToken(serviceClient, userId, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET); // Convert base64 to binary const binaryStr = atob(file_base64); const bytes = new Uint8Array(binaryStr.length); for (let i = 0; i < binaryStr.length; i++) { bytes[i] = binaryStr.charCodeAt(i); } // Create file metadata const metadata: any = { name: file_name }; if (parent_folder_id) { metadata.parents = [parent_folder_id]; } // Multipart upload const boundary = "----LovableBoundary" + Date.now(); const metaPart = JSON.stringify(metadata); const contentType = mime_type || "application/octet-stream"; const encoder = new TextEncoder(); const beforeFile = encoder.encode( `--${boundary}\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n${metaPart}\r\n--${boundary}\r\nContent-Type: ${contentType}\r\n\r\n` ); const afterFile = encoder.encode(`\r\n--${boundary}--`); const uploadBody = new Uint8Array(beforeFile.length + bytes.length + afterFile.length); uploadBody.set(beforeFile, 0); uploadBody.set(bytes, beforeFile.length); uploadBody.set(afterFile, beforeFile.length + bytes.length); const uploadRes = await fetch( "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&supportsAllDrives=true", { method: "POST", headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": `multipart/related; boundary=${boundary}`, }, body: uploadBody, } ); const uploadData = await uploadRes.json(); if (uploadData.error) { return jsonResponse(mapGoogleDriveError(uploadData.error)); } return jsonResponse({ success: true, file: uploadData }); } if (action === "create_folder") { if (!isAdmin) { return jsonResponse({ error: "Only admins can create Drive folders" }, 403); } const { folder_name, parent_folder_id: parentId } = body; if (!folder_name) { return jsonResponse({ error: "Missing folder_name" }, 400); } const accessToken = await getValidAccessToken(serviceClient, userId, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET); const metadata: any = { name: folder_name, mimeType: "application/vnd.google-apps.folder", }; if (parentId) metadata.parents = [parentId]; const createRes = await fetch( "https://www.googleapis.com/drive/v3/files?supportsAllDrives=true", { method: "POST", headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", }, body: JSON.stringify(metadata), } ); const createData = await createRes.json(); if (createData.error) { return jsonResponse(mapGoogleDriveError(createData.error)); } return jsonResponse({ success: true, folder: createData }); } return jsonResponse({ error: "Unknown action" }, 400); } catch (err: any) { console.error("google-drive-proxy error:", err); if (err?.message === "NOT_CONNECTED") { return jsonResponse({ error: "Google account not connected. Connect Google first.", code: "NOT_CONNECTED", }); } if (/invalid_grant|refresh token|REFRESH_FAILED/i.test(err?.message || "")) { return jsonResponse({ error: "Your Google connection expired. Disconnect Google and connect it again.", code: "RECONNECT_REQUIRED", reconnectRequired: true, }); } if (/oauth client was not found|invalid_client/i.test(err?.message || "")) { return jsonResponse({ error: "Google Drive OAuth credentials are invalid or missing. Update the Google client ID/secret in the backend, then reconnect Google Drive.", code: "OAUTH_CLIENT_NOT_FOUND", reconnectRequired: true, }); } return jsonResponse({ error: err.message }, 500); } });