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