Files
2026-06-01 20:19:26 -04:00

160 lines
5.7 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",
};
function json(body: unknown, status = 200) {
return new Response(JSON.stringify(body), {
status,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
Deno.serve(async (req) => {
if (req.method === "OPTIONS") return new Response("ok", { headers: corsHeaders });
try {
const { code, email, password, full_name } = await req.json();
if (!code || !email || !password) {
return json({ error: "Missing required fields" }, 400);
}
if (String(password).length < 8) {
return json({ error: "Password must be at least 8 characters" }, 400);
}
const admin = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!,
{ auth: { autoRefreshToken: false, persistSession: false } }
);
// Validate code
const { data: codeRow, error: codeErr } = await admin
.from("signup_codes")
.select("*")
.eq("code", String(code).trim())
.maybeSingle();
if (codeErr || !codeRow) return json({ error: "Invalid sign-up code" }, 404);
if (codeRow.redeemed_at) return json({ error: "This code has already been used" }, 410);
if (codeRow.expires_at && new Date(codeRow.expires_at) < new Date()) {
return json({ error: "This code has expired" }, 410);
}
const normalizedEmail = String(email).trim().toLowerCase();
// Create the auth user, auto-confirmed. If the email already exists,
// update the existing user's password and reuse their account so the
// signup code can still link them to the owner/rental records.
let userId: string | null = null;
const { data: created, error: createErr } = await admin.auth.admin.createUser({
email: normalizedEmail,
password: String(password),
email_confirm: true,
user_metadata: { full_name: full_name || null },
});
if (created?.user) {
userId = created.user.id;
} else {
const msg = (createErr?.message || "").toLowerCase();
const code = (createErr as any)?.code || (createErr as any)?.error_code;
const isDuplicate =
code === "email_exists" ||
msg.includes("already been registered") ||
msg.includes("already registered") ||
msg.includes("already exists");
if (!isDuplicate) {
return json({ error: createErr?.message || "Failed to create account" }, 400);
}
// Find the existing user by email and reset their password so they can sign in
let existingId: string | null = null;
let page = 1;
while (page <= 20 && !existingId) {
const { data: list, error: listErr } = await admin.auth.admin.listUsers({ page, perPage: 200 });
if (listErr) break;
const match = list?.users?.find((u) => (u.email || "").toLowerCase() === normalizedEmail);
if (match) existingId = match.id;
if (!list?.users || list.users.length < 200) break;
page++;
}
if (!existingId) {
return json({ error: "An account with this email already exists. Please sign in instead." }, 409);
}
const { error: updErr } = await admin.auth.admin.updateUserById(existingId, {
password: String(password),
email_confirm: true,
user_metadata: { full_name: full_name || null },
});
if (updErr) return json({ error: updErr.message }, 400);
userId = existingId;
}
if (!userId) {
return json({ error: "Failed to resolve user account" }, 500);
}
// Replace default 'homeowner' role from handle_new_user trigger if a different role was assigned
if (codeRow.role && codeRow.role !== "homeowner") {
await admin.from("user_roles").delete().eq("user_id", userId).eq("role", "homeowner");
await admin.from("user_roles").insert({ user_id: userId, role: codeRow.role });
}
// Optional owner pin: link the new auth user to the owner record
if (codeRow.owner_id) {
await admin
.from("owners")
.update({ user_id: userId })
.eq("id", codeRow.owner_id);
}
// Link RV/Boat lot rentals so the renter/owner portal can find them.
// Strategy:
// 1. If the code pinned a specific rental_id, link that one.
// 2. Otherwise, backfill any active rentals that match the pinned owner_id
// or whose renter_email matches the new account.
const isRvRole = codeRow.role === "rv_boat_lot" || codeRow.role === "rv_renter" || codeRow.role === "rv_owner";
if (isRvRole) {
const rentalUpdate: Record<string, unknown> = { user_id: userId };
if (codeRow.role === "rv_owner") rentalUpdate.is_owner = true;
if (codeRow.rental_id) {
await admin
.from("rv_boat_lot_rentals")
.update(rentalUpdate)
.eq("id", codeRow.rental_id);
} else {
// Match by owner_id (if pinned) OR renter_email
const orParts: string[] = [];
if (codeRow.owner_id) orParts.push(`owner_id.eq.${codeRow.owner_id}`);
orParts.push(`renter_email.eq.${normalizedEmail}`);
await admin
.from("rv_boat_lot_rentals")
.update(rentalUpdate)
.eq("status", "active")
.or(orParts.join(","));
}
}
// Mark code redeemed
await admin
.from("signup_codes")
.update({
redeemed_at: new Date().toISOString(),
redeemed_by_user_id: userId,
redeemed_email: normalizedEmail,
})
.eq("id", codeRow.id);
return json({ success: true, role: codeRow.role });
} catch (e) {
return json({ error: (e as Error).message || "Unexpected error" }, 500);
}
});