Add ACMCC app source, Supabase backend, and project config

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 20:19:26 -04:00
parent 313b51b412
commit 183fe0a93c
1422 changed files with 259271 additions and 0 deletions
@@ -0,0 +1,109 @@
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.99.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",
};
const PRODUCTION_ORIGIN = "https://avria.cloud";
Deno.serve(async (req) => {
if (req.method === "OPTIONS") return new Response("ok", { headers: corsHeaders });
try {
const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!;
const SERVICE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
const ANON_KEY = Deno.env.get("SUPABASE_ANON_KEY")!;
const authHeader = req.headers.get("Authorization") || "";
if (!authHeader) {
return new Response(JSON.stringify({ error: "Missing auth" }), { status: 401, headers: { ...corsHeaders, "Content-Type": "application/json" } });
}
// Verify caller is staff
const userClient = createClient(SUPABASE_URL, ANON_KEY, { global: { headers: { Authorization: authHeader } } });
const { data: { user: caller } } = await userClient.auth.getUser();
if (!caller) {
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { ...corsHeaders, "Content-Type": "application/json" } });
}
const admin = createClient(SUPABASE_URL, SERVICE_KEY);
const { data: roles } = await admin.from("user_roles").select("role").eq("user_id", caller.id);
const isStaff = (roles || []).some((r: any) => r.role === "admin" || r.role === "manager");
if (!isStaff) {
return new Response(JSON.stringify({ error: "Staff only" }), { status: 403, headers: { ...corsHeaders, "Content-Type": "application/json" } });
}
const body = await req.json();
const { rental_id, email, as_owner } = body || {};
const isOwnerInvite = !!as_owner;
if (!rental_id || !email) {
return new Response(JSON.stringify({ error: "rental_id and email required" }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } });
}
// Look up rental
const { data: rental, error: rentalErr } = await admin
.from("rv_boat_lot_rentals")
.select("id, renter_name, association_id, user_id")
.eq("id", rental_id)
.single();
if (rentalErr || !rental) {
return new Response(JSON.stringify({ error: "Rental not found" }), { status: 404, headers: { ...corsHeaders, "Content-Type": "application/json" } });
}
// Find or create user
let userId: string | null = null;
const { data: existingList } = await admin.auth.admin.listUsers({ page: 1, perPage: 1, filter: `email.eq.${email}` } as any);
// Fallback: paginate-search if filter unsupported
if (existingList?.users?.length) {
userId = existingList.users[0].id;
} else {
// Try fetching all (small project) then match
const { data: all } = await admin.auth.admin.listUsers({ page: 1, perPage: 1000 });
const found = all?.users?.find((u) => (u.email || "").toLowerCase() === String(email).toLowerCase());
if (found) userId = found.id;
}
let invited = false;
if (!userId) {
const { data: created, error: createErr } = await admin.auth.admin.inviteUserByEmail(email, {
redirectTo: `${PRODUCTION_ORIGIN}/reset-password?mode=invite`,
data: { full_name: rental.renter_name },
});
if (createErr || !created?.user) {
return new Response(JSON.stringify({ error: createErr?.message || "Failed to invite" }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } });
}
userId = created.user.id;
invited = true;
} else {
// Send password recovery so existing users can also access portal
await admin.auth.admin.generateLink({ type: "recovery", email, options: { redirectTo: `${PRODUCTION_ORIGIN}/reset-password` } } as any);
}
// Assign the homeowner-style RV/Boat Lot role, idempotent
const role = "rv_boat_lot";
await admin.from("user_roles").upsert(
{ user_id: userId, role: role as any },
{ onConflict: "user_id,role" } as any,
);
// Link rental to user (and flag is_owner if this is an owner invite)
const updatePayload: Record<string, unknown> = { user_id: userId, renter_email: email };
if (isOwnerInvite) updatePayload.is_owner = true;
const { error: linkErr } = await admin
.from("rv_boat_lot_rentals")
.update(updatePayload)
.eq("id", rental_id);
if (linkErr) {
return new Response(JSON.stringify({ error: linkErr.message }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } });
}
return new Response(JSON.stringify({ ok: true, user_id: userId, invited }), {
status: 200,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
} catch (e: any) {
return new Response(JSON.stringify({ error: e?.message || "Server error" }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } });
}
});