mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
183fe0a93c
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
199 lines
6.8 KiB
TypeScript
199 lines
6.8 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",
|
|
};
|
|
|
|
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 new Response(JSON.stringify({ error: "Google OAuth credentials not configured" }), {
|
|
status: 500,
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
const body = await req.json();
|
|
const { action, code, redirect_uri } = body;
|
|
const serviceClient = createClient(SUPABASE_URL, SERVICE_ROLE_KEY);
|
|
|
|
// Authenticate user
|
|
const authHeader = req.headers.get("Authorization");
|
|
if (!authHeader?.startsWith("Bearer ")) {
|
|
// For check_status, return not-connected instead of 401
|
|
if (action === "check_status") {
|
|
return new Response(JSON.stringify({ connected: false }), {
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
|
status: 401,
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
const token = authHeader.replace("Bearer ", "");
|
|
let userId: string | null = null;
|
|
|
|
try {
|
|
const payload = token.split(".")[1];
|
|
if (!payload) throw new Error("Missing JWT payload");
|
|
const normalized = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
|
|
const decoded = JSON.parse(atob(padded));
|
|
userId = typeof decoded?.sub === "string" ? decoded.sub : null;
|
|
} catch (error) {
|
|
console.error("google-drive-auth token decode failed", error);
|
|
}
|
|
|
|
if (!userId) {
|
|
if (action === "check_status") {
|
|
return new Response(JSON.stringify({ connected: false }), {
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
|
status: 401,
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
// Check staff role (Google Drive is staff-only)
|
|
const { data: roles } = await serviceClient
|
|
.from("user_roles")
|
|
.select("role")
|
|
.eq("user_id", userId);
|
|
|
|
const STAFF_ROLES = ["admin", "manager", "employee", "staff"];
|
|
const isStaff = roles?.some((r: any) => STAFF_ROLES.includes(r.role));
|
|
if (!isStaff) {
|
|
if (action === "check_status") {
|
|
return new Response(JSON.stringify({ connected: false }), {
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
return new Response(JSON.stringify({ error: "Only staff can connect Google Drive" }), {
|
|
status: 403,
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
if (action === "get_auth_url") {
|
|
const scopes = [
|
|
"https://www.googleapis.com/auth/drive",
|
|
"https://www.googleapis.com/auth/calendar.readonly",
|
|
].join(" ");
|
|
|
|
const params = new URLSearchParams({
|
|
client_id: GOOGLE_CLIENT_ID,
|
|
redirect_uri: redirect_uri,
|
|
response_type: "code",
|
|
scope: scopes,
|
|
access_type: "offline",
|
|
prompt: "consent",
|
|
state: "drive_connect",
|
|
});
|
|
|
|
return new Response(JSON.stringify({ url: `https://accounts.google.com/o/oauth2/v2/auth?${params}` }), {
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
if (action === "exchange_code") {
|
|
if (!code || !redirect_uri) {
|
|
return new Response(JSON.stringify({ error: "Missing code or redirect_uri" }), {
|
|
status: 400,
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
const tokenRes = await fetch("https://oauth2.googleapis.com/token", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
body: new URLSearchParams({
|
|
client_id: GOOGLE_CLIENT_ID,
|
|
client_secret: GOOGLE_CLIENT_SECRET,
|
|
code,
|
|
grant_type: "authorization_code",
|
|
redirect_uri,
|
|
}),
|
|
});
|
|
|
|
const tokenData = await tokenRes.json();
|
|
if (tokenData.error) {
|
|
return new Response(JSON.stringify({ error: tokenData.error_description || tokenData.error }), {
|
|
status: 400,
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
const expiresAt = new Date(Date.now() + tokenData.expires_in * 1000).toISOString();
|
|
|
|
const { error: upsertError } = await serviceClient
|
|
.from("google_drive_tokens")
|
|
.upsert({
|
|
user_id: userId,
|
|
access_token: tokenData.access_token,
|
|
refresh_token: tokenData.refresh_token,
|
|
token_expires_at: expiresAt,
|
|
}, { onConflict: "user_id" });
|
|
|
|
if (upsertError) {
|
|
return new Response(JSON.stringify({ error: "Failed to store tokens" }), {
|
|
status: 500,
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
return new Response(JSON.stringify({ success: true }), {
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
if (action === "check_status") {
|
|
const { data: tokenRow } = await serviceClient
|
|
.from("google_drive_tokens")
|
|
.select("id, token_expires_at")
|
|
.eq("user_id", userId)
|
|
.maybeSingle();
|
|
|
|
return new Response(JSON.stringify({ connected: !!tokenRow }), {
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
if (action === "disconnect") {
|
|
await serviceClient
|
|
.from("google_drive_tokens")
|
|
.delete()
|
|
.eq("user_id", userId);
|
|
|
|
return new Response(JSON.stringify({ success: true }), {
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
return new Response(JSON.stringify({ error: "Unknown action" }), {
|
|
status: 400,
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
});
|
|
} catch (err) {
|
|
console.error("google-drive-auth error:", err);
|
|
return new Response(JSON.stringify({ error: err.message }), {
|
|
status: 500,
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
});
|