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.
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.
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" }),