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>
151 lines
5.2 KiB
TypeScript
151 lines
5.2 KiB
TypeScript
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
|
|
|
|
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 authHeader = req.headers.get("Authorization");
|
|
if (!authHeader) {
|
|
return new Response(JSON.stringify({ error: "Missing authorization" }), {
|
|
status: 401,
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
|
|
const supabaseAnonKey = Deno.env.get("SUPABASE_ANON_KEY")!;
|
|
const serviceRoleKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
|
|
|
|
// Validate user via JWT claims (works with signing-keys auth)
|
|
const token = authHeader.replace("Bearer ", "");
|
|
const authClient = createClient(supabaseUrl, supabaseAnonKey);
|
|
const { data: claimsData, error: userError } = await authClient.auth.getClaims(token);
|
|
if (userError || !claimsData?.claims?.sub) {
|
|
console.error("Auth failed:", userError);
|
|
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
|
status: 401,
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
const userId = claimsData.claims.sub;
|
|
|
|
// Check admin role
|
|
const serviceClient = createClient(supabaseUrl, serviceRoleKey);
|
|
const { data: roles } = await serviceClient
|
|
.from("user_roles")
|
|
.select("role")
|
|
.eq("user_id", userId);
|
|
|
|
const isAdmin = roles?.some((r: any) => ["admin", "manager"].includes(r.role));
|
|
if (!isAdmin) {
|
|
return new Response(JSON.stringify({ error: "Forbidden" }), {
|
|
status: 403,
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
const url = new URL(req.url);
|
|
const mappingId = url.searchParams.get("mapping_id");
|
|
const limit = Math.min(parseInt(url.searchParams.get("limit") || "10"), 25);
|
|
|
|
// Fetch the stripe account mapping(s)
|
|
let query = serviceClient
|
|
.from("stripe_account_mappings")
|
|
.select("id, stripe_secret_key, label, association_id, associations(name)")
|
|
.eq("is_active", true);
|
|
|
|
if (mappingId) {
|
|
query = query.eq("id", mappingId);
|
|
}
|
|
|
|
const { data: mappings, error: mapError } = await query;
|
|
if (mapError) throw mapError;
|
|
|
|
if (!mappings || mappings.length === 0) {
|
|
return new Response(JSON.stringify({ transactions: [], accounts: [] }), {
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
// If no specific mapping requested, return accounts list + transactions from first account
|
|
const accounts = mappings.map((m: any) => ({
|
|
id: m.id,
|
|
name: m.label || (m.associations as any)?.name || "Unknown Account",
|
|
hasSecretKey: !!m.stripe_secret_key,
|
|
}));
|
|
|
|
// Pick which mapping to fetch transactions from
|
|
const targetMapping = mappingId
|
|
? mappings[0]
|
|
: mappings[0]; // default to first
|
|
|
|
if (!targetMapping?.stripe_secret_key) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
transactions: [],
|
|
accounts,
|
|
error: "No secret key configured for this account.",
|
|
}),
|
|
{ headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
|
);
|
|
}
|
|
|
|
// Fetch recent charges from Stripe
|
|
const stripeRes = await fetch(
|
|
`https://api.stripe.com/v1/charges?limit=${limit}&expand[]=data.customer`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${targetMapping.stripe_secret_key}`,
|
|
},
|
|
}
|
|
);
|
|
|
|
if (!stripeRes.ok) {
|
|
const errBody = await stripeRes.text();
|
|
console.error("Stripe API error:", stripeRes.status, errBody);
|
|
return new Response(
|
|
JSON.stringify({
|
|
transactions: [],
|
|
accounts,
|
|
error: `Stripe API error: ${stripeRes.status}`,
|
|
}),
|
|
{ headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
|
);
|
|
}
|
|
|
|
const stripeData = await stripeRes.json();
|
|
|
|
const transactions = (stripeData.data || []).map((charge: any) => ({
|
|
id: charge.id,
|
|
amount: charge.amount,
|
|
currency: charge.currency,
|
|
status: charge.status,
|
|
created: charge.created,
|
|
description: charge.description || null,
|
|
customer_name: charge.customer?.name || null,
|
|
customer_email: charge.customer?.email || charge.billing_details?.email || null,
|
|
payment_method: charge.payment_method_details?.type || null,
|
|
}));
|
|
|
|
return new Response(
|
|
JSON.stringify({ transactions, accounts }),
|
|
{ headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
|
);
|
|
} catch (error: unknown) {
|
|
console.error("Error in stripe-transactions:", error);
|
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
return new Response(JSON.stringify({ error: message }), {
|
|
status: 500,
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
});
|