Skip to content
Draft
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
7 changes: 5 additions & 2 deletions app/(auth)/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,15 +119,18 @@ export async function updateUser(formData: UpdatePasswordFormData) {
redirect("/dashboard");
}

// resend confirmation email
export async function resendConfirmationEmail(email: string) {
export async function resendConfirmationEmail(
email: string,
captchaToken: string,
) {
const supabase = createClient();

const { error } = await supabase.auth.resend({
type: "signup",
email: email,
options: {
emailRedirectTo: `${getURL()}/dashboard`,
captchaToken: captchaToken,
},
});

Expand Down
90 changes: 90 additions & 0 deletions app/(auth)/auth/verified/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"use client";

import React, { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { createClient } from "@/utils/supabase/client";
import { useRouter } from "next/navigation";
import { toast } from "sonner";

export default function VerifiedPage() {
const [email, setEmail] = useState<string>("");
const [isLoading, setIsLoading] = useState(false);
const router = useRouter();

useEffect(() => {
const storedEmail = localStorage.getItem("verificationEmail");
if (storedEmail) {
setEmail(storedEmail);
}
}, []);

const signInWithToken = async () => {
if (!email) {
router.push("/signin");
return;
}

setIsLoading(true);

try {
const supabase = createClient();
const { error } = await supabase.auth.signInWithOtp({
email: email,
options: {
shouldCreateUser: false,
emailRedirectTo: `${window.location.origin}/dashboard`,
},
});

if (error) {
toast.error(
"Failed to sign in automatically. Please sign in manually.",
);
router.push(`/signin?email=${encodeURIComponent(email)}`);
return;
}

toast.success("Magic link sent! Check your email to complete sign in.");
} catch (error) {
toast.error("An error occurred. Please sign in manually.");
router.push(`/signin?email=${encodeURIComponent(email)}`);
} finally {
setIsLoading(false);
}
};

return (
<div className="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<h2 className="mt-10 text-center text-3xl font-bold leading-9 text-primary-700 dark:text-primary-400">
Email Verified Successfully!
</h2>
<p className="mt-5 text-center text-lg text-gray-600 dark:text-gray-300">
Your email has been verified and your account is now active.
</p>

<div className="mt-8 flex flex-col gap-4">
{email ? (
<>
<Button
onClick={signInWithToken}
className="w-full"
disabled={isLoading}
>
{isLoading ? "Sending magic link..." : "Sign In Automatically"}
</Button>
<p className="text-center text-sm text-gray-500 dark:text-gray-400">
We&apos;ll send a magic link to your email for a seamless login.
</p>
</>
) : (
<Link href="/signin">
<Button className="w-full">Sign In Now</Button>
</Link>
)}
</div>
</div>
</div>
);
}
35 changes: 35 additions & 0 deletions app/(auth)/auth/verify/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { NextRequest, NextResponse } from "next/server";
import { createClient } from "@/utils/supabase/server";

export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const token_hash = searchParams.get("token_hash");
const type = searchParams.get("type");

if (!token_hash || !type) {
return NextResponse.redirect(
new URL("/signin?error=missing-token", request.url),
);
}

const supabase = createClient();

try {
const { error } = await supabase.auth.verifyOtp({
token_hash,
type: type as any,
});

if (error) {
return NextResponse.redirect(
new URL(`/signin?error=${error.message}`, request.url),
);
}

return NextResponse.redirect(new URL("/auth/verified", request.url));
} catch (error) {
return NextResponse.redirect(
new URL("/signin?error=verification-failed", request.url),
);
}
}
15 changes: 14 additions & 1 deletion components/auth/signin.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";
import Link from "next/link";
import { useState, useRef } from "react";
import { useState, useRef, useEffect } from "react";
import HCaptcha from "@hcaptcha/react-hcaptcha";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
Expand Down Expand Up @@ -39,7 +39,20 @@ export default function SignIn() {
password: "",
},
});
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const emailParam = params.get("email");
if (emailParam) {
form.setValue("email", emailParam);
}

if (!emailParam) {
const storedEmail = localStorage.getItem("verificationEmail");
if (storedEmail) {
form.setValue("email", storedEmail);
}
}
}, [form]);
const handleSignIn = async (data: SignInFormData) => {
if (isSubmitting) return;
setIsSubmitting(true);
Expand Down
2 changes: 1 addition & 1 deletion components/auth/signup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Link from "next/link";
import { useState, useRef } from "react";
import { signinWithOAuth } from "@/app/(auth)/actions";
import { HCAPTCHA_SITE_KEY_PUBLIC } from "@/utils/constants";
import { AdminUserAttributes, Provider } from "@supabase/supabase-js";
import { Provider } from "@supabase/supabase-js";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { Button } from "@/components/ui/button";
Expand Down
Loading