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,198 @@
|
||||
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" },
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user