mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 01:40:01 +00:00
183fe0a93c
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
160 lines
5.7 KiB
TypeScript
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);
|
|
}
|
|
});
|