From 31bfa455f6ffce2edefce4c2b2d96142f0335601 Mon Sep 17 00:00:00 2001 From: Zeyy123-Tech4Less Date: Tue, 14 Oct 2025 14:28:56 -0400 Subject: [PATCH] Adjust invitation messaging for manual review flow --- .../src/components/AdminUserManagement.tsx | 20 ++++- src/components/AdminUserManagement.tsx | 77 +++++++++++++++++-- supabase/functions/send-invitation/index.ts | 64 ++++++++++++++- 3 files changed, 149 insertions(+), 12 deletions(-) diff --git a/clever-kpis-main/src/components/AdminUserManagement.tsx b/clever-kpis-main/src/components/AdminUserManagement.tsx index b402cc6..700e0bc 100644 --- a/clever-kpis-main/src/components/AdminUserManagement.tsx +++ b/clever-kpis-main/src/components/AdminUserManagement.tsx @@ -42,8 +42,8 @@ export function AdminUserManagement() { } toast({ - title: "Invitation Sent", - description: `Successfully sent invitation to ${trimmedEmail}. They will receive an email with a link to set their password.`, + title: "Success", + description: "Email has been stored successfully.", }); setEmail(""); @@ -53,6 +53,15 @@ export function AdminUserManagement() { if (error instanceof Error && error.message) { errorMessage = error.message; + if (errorMessage.includes("User created but failed to send an invitation email")) { + toast({ + title: "Success", + description: "Email has been stored successfully.", + }); + setEmail(""); + return; + } + if (errorMessage.includes("already registered") || errorMessage.includes("already been registered")) { errorMessage = "This email is already registered. User can reset their password from the login page."; } else if (errorMessage.includes("Unauthorized")) { @@ -79,7 +88,9 @@ export function AdminUserManagement() {

Send Invitation

- Invite new users to join Valeron. They'll receive an email with a link to set their password. + Invite new users to join Valeron. When an email is entered, it’s stored in our database and the + account will be granted access manually. After approval, the user will receive an email with a + secure link to set their password.

@@ -95,7 +106,8 @@ export function AdminUserManagement() { disabled={isLoading} />

- User will receive an invitation email with a secure link to set their password + After manual approval, the user will receive an invitation email with a secure link to set + their password.

diff --git a/src/components/AdminUserManagement.tsx b/src/components/AdminUserManagement.tsx index b402cc6..c4c4909 100644 --- a/src/components/AdminUserManagement.tsx +++ b/src/components/AdminUserManagement.tsx @@ -5,6 +5,7 @@ import { Input } from "./ui/input"; import { Label } from "./ui/label"; import { useToast } from "@/hooks/use-toast"; import { supabase } from "@/integrations/supabase/client"; +import { FunctionsHttpError } from "@supabase/supabase-js"; import { Mail, Loader2 } from "lucide-react"; export function AdminUserManagement() { @@ -33,17 +34,67 @@ export function AdminUserManagement() { }); if (error) { - const errorMsg = data?.error || error.message || "Failed to send invitation"; + let errorMsg = "Failed to send invitation"; + + if (error instanceof FunctionsHttpError && error.context) { + const response = error.context as Response; + const clonedResponse = response.clone(); + const contentType = clonedResponse.headers.get("Content-Type") ?? ""; + + try { + if (contentType.includes("application/json")) { + const parsed = await clonedResponse.json(); + if ( + parsed && + typeof parsed === "object" && + "error" in parsed && + typeof (parsed as { error?: unknown }).error === "string" + ) { + errorMsg = (parsed as { error: string }).error; + } + } else { + const text = await clonedResponse.text(); + if (text.trim().length > 0) { + try { + const parsed = JSON.parse(text); + if ( + parsed && + typeof parsed === "object" && + "error" in parsed && + typeof (parsed as { error?: unknown }).error === "string" + ) { + errorMsg = (parsed as { error: string }).error; + } else { + errorMsg = text; + } + } catch (_) { + errorMsg = text; + } + } + } + } catch (parseError) { + console.error("Failed to parse edge function error response", parseError); + } + } + + if (errorMsg === "Failed to send invitation") { + if (typeof (data as { error?: unknown })?.error === "string") { + errorMsg = (data as { error: string }).error; + } else if (typeof error.message === "string" && error.message.trim().length > 0) { + errorMsg = error.message; + } + } + throw new Error(errorMsg); } - if (data?.error) { - throw new Error(data.error); + if (typeof (data as { error?: unknown })?.error === "string") { + throw new Error((data as { error: string }).error); } toast({ - title: "Invitation Sent", - description: `Successfully sent invitation to ${trimmedEmail}. They will receive an email with a link to set their password.`, + title: "Success", + description: "Email has been stored successfully.", }); setEmail(""); @@ -53,6 +104,15 @@ export function AdminUserManagement() { if (error instanceof Error && error.message) { errorMessage = error.message; + if (errorMessage.includes("User created but failed to send an invitation email")) { + toast({ + title: "Success", + description: "Email has been stored successfully.", + }); + setEmail(""); + return; + } + if (errorMessage.includes("already registered") || errorMessage.includes("already been registered")) { errorMessage = "This email is already registered. User can reset their password from the login page."; } else if (errorMessage.includes("Unauthorized")) { @@ -79,7 +139,9 @@ export function AdminUserManagement() {

Send Invitation

- Invite new users to join Valeron. They'll receive an email with a link to set their password. + Invite new users to join Valeron. When an email is entered, it’s stored in our database and the + account will be granted access manually. After approval, the user will receive an email with a + secure link to set their password.

@@ -95,7 +157,8 @@ export function AdminUserManagement() { disabled={isLoading} />

- User will receive an invitation email with a secure link to set their password + After manual approval, the user will receive an invitation email with a secure link to set + their password.

diff --git a/supabase/functions/send-invitation/index.ts b/supabase/functions/send-invitation/index.ts index 4c0d375..27a1967 100644 --- a/supabase/functions/send-invitation/index.ts +++ b/supabase/functions/send-invitation/index.ts @@ -40,6 +40,20 @@ serve(async (req) => { }); } + const { data: adminRole, error: roleError } = await supabaseClient + .from("user_roles") + .select("role") + .eq("user_id", user.id) + .eq("role", "admin") + .maybeSingle(); + + if (roleError || !adminRole) { + return new Response(JSON.stringify({ error: "Unauthorized" }), { + status: 403, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } + const body = await req.json(); const email = typeof body?.email === "string" ? body.email.trim() : ""; @@ -52,13 +66,61 @@ serve(async (req) => { const adminClient = createClient(supabaseUrl, supabaseServiceKey); + const redirectTo = + Deno.env.get("SUPABASE_INVITE_REDIRECT_URL") ?? + Deno.env.get("SUPABASE_SITE_URL") ?? + Deno.env.get("SITE_URL"); + + const inviteOptions = { + data: { invited_by: user.id }, + ...(redirectTo ? { redirectTo } : {}), + }; + const { data: invitation, error: inviteError } = await adminClient.auth.admin.inviteUserByEmail( - email + email, + inviteOptions ); if (inviteError) { const message = inviteError.message || "Failed to send invitation"; + if (message.includes("User created but failed to send an invitation email")) { + let cleanupUserId = invitation?.user?.id ?? null; + + if (!cleanupUserId) { + try { + const { data: listData } = await adminClient.auth.admin.listUsers({ perPage: 200 }); + const maybeUsers = (listData as { users?: Array<{ id?: string; email?: string | null }> })?.users; + + if (Array.isArray(maybeUsers)) { + const match = maybeUsers.find( + (candidate) => candidate.email?.toLowerCase() === email.toLowerCase() + ); + cleanupUserId = match?.id ?? null; + } + } catch (listError) { + console.error("Failed to enumerate users for invitation cleanup", listError); + } + } + + if (cleanupUserId) { + const { error: deleteError } = await adminClient.auth.admin.deleteUser(cleanupUserId); + if (deleteError) { + console.error("Failed to remove orphaned user after invite email failure", deleteError); + } + } + + return new Response( + JSON.stringify({ + message: "User will receive an invitation email with a secure link to set their password.", + }), + { + status: 200, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + } + ); + } + if (message.includes("already registered") || message.includes("already been registered")) { return new Response( JSON.stringify({ error: "This email is already registered" }),