Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions clever-kpis-main/src/components/AdminUserManagement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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("");
Expand All @@ -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")) {
Expand All @@ -79,7 +88,9 @@ export function AdminUserManagement() {
<div className="flex-1">
<h3 className="text-lg font-semibold text-foreground mb-2">Send Invitation</h3>
<p className="text-sm text-muted-foreground mb-4">
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.
</p>

<form onSubmit={handleSendInvitation} className="space-y-4">
Expand All @@ -95,7 +106,8 @@ export function AdminUserManagement() {
disabled={isLoading}
/>
<p className="text-xs text-muted-foreground">
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.
</p>
</div>

Expand Down
77 changes: 70 additions & 7 deletions src/components/AdminUserManagement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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("");
Expand All @@ -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")) {
Expand All @@ -79,7 +139,9 @@ export function AdminUserManagement() {
<div className="flex-1">
<h3 className="text-lg font-semibold text-foreground mb-2">Send Invitation</h3>
<p className="text-sm text-muted-foreground mb-4">
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.
</p>

<form onSubmit={handleSendInvitation} className="space-y-4">
Expand All @@ -95,7 +157,8 @@ export function AdminUserManagement() {
disabled={isLoading}
/>
<p className="text-xs text-muted-foreground">
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.
</p>
</div>

Expand Down
64 changes: 63 additions & 1 deletion supabase/functions/send-invitation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() : "";

Expand All @@ -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" }),
Expand Down