Files
2026-06-01 20:19:26 -04:00

392 lines
13 KiB
TypeScript

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<string> {
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);
}
});