fix(security): remove the dead email-verification function

The function was never used and carried a real hole while sitting there.

`send` took an email address from the request body, did not check who was
calling, and returned the verification token in the response — with a
`// Remove in production` comment on that line. Anyone could mint a token for
another account's address and immediately use it. `Access-Control-Allow-Origin`
was `*`.

It was never reachable in practice: nothing calls `send`, so no token was ever
created, so `verify` could never succeed. The database confirms it —
`email_verification_tokens` has zero rows, ever.

Real email verification runs through Supabase Auth, not this function.
EmailVerification.tsx handles `type=signup` / `email_change` via the URL hash and
`getSession()`, and Dashboard.tsx resends through `auth.resend()`. Those paths
are untouched. The `?token=` branch that called this function was dead code
fed by a token nothing produced, so it goes with it.

Removing it also took the `verifying` state with it: `setVerifying` had no
remaining callers, so the spinner could never render and every `!verifying`
guard was permanently true — conditions that read as if the value could vary.

Fixing this properly instead of deleting it would mean building the email
delivery the function never had ("In production, send email via email service"),
duplicating what Supabase Auth already does correctly.
This commit is contained in:
2026-07-31 11:04:13 -07:00
parent b20cfc84ef
commit c0e1326c4b
2 changed files with 6 additions and 136 deletions
+6 -36
View File
@@ -10,7 +10,6 @@ export default function EmailVerification() {
const [searchParams] = useSearchParams();
const location = useLocation();
const navigate = useNavigate();
const [verifying, setVerifying] = useState(false);
const [verified, setVerified] = useState(false);
const [error, setError] = useState('');
const [resending, setResending] = useState(false);
@@ -19,7 +18,6 @@ export default function EmailVerification() {
// Get email from navigation state (after sign up)
const email = location.state?.email;
const token = searchParams.get('token');
const type = searchParams.get('type');
useEffect(() => {
@@ -32,29 +30,8 @@ export default function EmailVerification() {
setVerified(true);
}
});
} else if (token) {
verifyEmail(token);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [token, type]);
const verifyEmail = async (verifyToken: string) => {
setVerifying(true);
try {
const { error } = await supabase.functions.invoke('email-verification', {
body: { action: 'verify', token: verifyToken }
});
if (error) throw error;
setVerified(true);
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : t('emailVerify.failedToVerify');
setError(errorMessage);
} finally {
setVerifying(false);
}
};
}, [type]);
const handleResendEmail = async () => {
if (!email) return;
@@ -80,7 +57,7 @@ export default function EmailVerification() {
};
// Show "check your email" screen after sign up
if (email && !token && !type) {
if (email && !type) {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-black to-gray-900 flex items-center justify-center p-4">
<Card className="w-full max-w-md relative bg-gray-900/90 backdrop-blur-xl border-gray-800">
@@ -162,18 +139,11 @@ export default function EmailVerification() {
<CardHeader>
<CardTitle className="text-white">{t('emailVerify.title')}</CardTitle>
<CardDescription className="text-gray-400">
{verifying ? t('emailVerify.verifyingEmail') : t('emailVerify.verificationStatus')}
{t('emailVerify.verificationStatus')}
</CardDescription>
</CardHeader>
<CardContent className="text-center space-y-4">
{verifying && (
<div className="flex flex-col items-center space-y-4">
<Loader2 className="h-12 w-12 animate-spin text-green-500" />
<p className="text-gray-300">{t('emailVerify.pleaseWait')}</p>
</div>
)}
{!verifying && verified && (
{verified && (
<div className="flex flex-col items-center space-y-4">
<CheckCircle className="h-12 w-12 text-green-500" />
<h3 className="text-lg font-semibold text-white">{t('emailVerify.success')}</h3>
@@ -189,7 +159,7 @@ export default function EmailVerification() {
</div>
)}
{!verifying && !verified && error && (
{!verified && error && (
<div className="flex flex-col items-center space-y-4">
<XCircle className="h-12 w-12 text-red-500" />
<h3 className="text-lg font-semibold text-white">{t('emailVerify.failed')}</h3>
@@ -205,7 +175,7 @@ export default function EmailVerification() {
</div>
)}
{!verifying && !verified && !error && !token && !type && (
{!verified && !error && !type && (
<div className="flex flex-col items-center space-y-4">
<Mail className="h-12 w-12 text-gray-500" />
<h3 className="text-lg font-semibold text-white">{t('emailVerify.noToken')}</h3>
@@ -1,100 +0,0 @@
export const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers":
"authorization, x-client-info, apikey, content-type",
};
Deno.serve(async (req) => {
if (req.method === "OPTIONS") {
return new Response("ok", { headers: corsHeaders });
}
try {
const { action, token, email } = await req.json();
if (action === "send") {
// Generate verification token
const verificationToken = crypto.randomUUID();
// Store token in database (expires in 24 hours)
const { createClient } =
await import("https://esm.sh/@supabase/supabase-js@2");
const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
const supabaseKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
const supabase = createClient(supabaseUrl, supabaseKey);
const { data: user } = await supabase.auth.admin.getUserByEmail(email);
if (!user?.user) {
throw new Error("User not found");
}
await supabase.from("email_verification_tokens").insert({
user_id: user.user.id,
token: verificationToken,
email: email,
expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
});
// In production, send email via email service
// For now, return the token
return new Response(
JSON.stringify({
success: true,
message: "Verification email sent",
token: verificationToken, // Remove in production
}),
{ headers: { ...corsHeaders, "Content-Type": "application/json" } },
);
}
if (action === "verify") {
const { createClient } =
await import("https://esm.sh/@supabase/supabase-js@2");
const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
const supabaseKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
const supabase = createClient(supabaseUrl, supabaseKey);
// Check token validity
const { data: tokenData } = await supabase
.from("email_verification_tokens")
.select("*")
.eq("token", token)
.single();
if (!tokenData || new Date(tokenData.expires_at) < new Date()) {
throw new Error("Invalid or expired token");
}
// Update user profile
await supabase
.from("profiles")
.update({
email_verified: true,
email_verified_at: new Date().toISOString(),
})
.eq("id", tokenData.user_id);
// Delete used token
await supabase
.from("email_verification_tokens")
.delete()
.eq("token", token);
return new Response(
JSON.stringify({
success: true,
message: "Email verified successfully",
}),
{ headers: { ...corsHeaders, "Content-Type": "application/json" } },
);
}
throw new Error("Invalid action");
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
headers: { ...corsHeaders, "Content-Type": "application/json" },
status: 400,
});
}
});