mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
Add ACMCC app source, Supabase backend, and project config
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,391 @@
|
||||
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);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user