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>
110 lines
5.0 KiB
TypeScript
110 lines
5.0 KiB
TypeScript
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" } });
|
|
}
|
|
});
|