mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
Hostinger Reach integration UI + ARC Buildium matching, drop Mailchimp
- HostingerReachPage (replaces MailchimpPage): connect Reach via reach-connection, per-association segment sync via reach-sync - ARC Applications: Buildium import review/matching updates - buildium-import-stage/apply: latest staging + apply changes (already deployed to Supabase) - migrations: hostinger_reach_integration + arc_finalized_lock service role (already applied to live DB) - CI: note that deployment is VPS-side polling (auto-deploy.sh cron) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
// Hostinger Reach — connection test. Validates the stored global API token by listing segments.
|
||||
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",
|
||||
};
|
||||
|
||||
const REACH_BASE = "https://developers.hostinger.com/api/reach/v1";
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders });
|
||||
const json = (b: unknown, status = 200) =>
|
||||
new Response(JSON.stringify(b), { status, headers: { ...corsHeaders, "Content-Type": "application/json" } });
|
||||
|
||||
try {
|
||||
const authHeader = req.headers.get("Authorization");
|
||||
if (!authHeader) return json({ error: "Unauthorized" }, 401);
|
||||
|
||||
const userClient = createClient(
|
||||
Deno.env.get("SUPABASE_URL")!,
|
||||
Deno.env.get("SUPABASE_ANON_KEY")!,
|
||||
{ global: { headers: { Authorization: authHeader } } },
|
||||
);
|
||||
const admin = createClient(Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!);
|
||||
|
||||
const { data: { user } } = await userClient.auth.getUser();
|
||||
if (!user) return json({ error: "Unauthorized" }, 401);
|
||||
const { data: roles } = await admin.from("user_roles").select("role").eq("user_id", user.id);
|
||||
if (!(roles || []).some((r: any) => r.role === "admin")) return json({ error: "Admin only" }, 403);
|
||||
|
||||
// A token can be supplied in the body to test before saving; otherwise use the stored one.
|
||||
const body = await req.json().catch(() => ({}));
|
||||
let token: string | null = typeof body.api_token === "string" && body.api_token.trim() ? body.api_token.trim() : null;
|
||||
if (!token) {
|
||||
const { data: cfg } = await admin
|
||||
.from("hostinger_reach_config").select("api_token").order("updated_at", { ascending: false }).limit(1).maybeSingle();
|
||||
token = cfg?.api_token || null;
|
||||
}
|
||||
if (!token) return json({ success: false, error: "No Hostinger Reach API token configured." }, 400);
|
||||
|
||||
const res = await fetch(`${REACH_BASE}/segmentation/segments`, {
|
||||
headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
|
||||
});
|
||||
const text = await res.text();
|
||||
if (!res.ok) {
|
||||
return json({ success: false, error: `Reach API ${res.status}: ${text.slice(0, 300)}` }, 200);
|
||||
}
|
||||
let parsed: any = {};
|
||||
try { parsed = JSON.parse(text); } catch { /* ignore */ }
|
||||
const list = Array.isArray(parsed) ? parsed : (parsed.data ?? parsed.segments ?? []);
|
||||
return json({ success: true, segment_count: Array.isArray(list) ? list.length : 0 });
|
||||
} catch (err) {
|
||||
return json({ success: false, error: (err as Error).message }, 500);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user