From f8d5c6cd5675e2f4daaa6c89120a73cb6f047081 Mon Sep 17 00:00:00 2001
From: Rudra Patel <85089368+RudraPatel2003@users.noreply.github.com>
Date: Thu, 26 Dec 2024 17:39:24 -0600
Subject: [PATCH] feat: Fix sign in callbackUrl bug and prevent signOut when
verifying email (#81)
---
next.config.mjs | 15 ++++++-
src/app/api/auth/[...nextauth]/options.ts | 9 +++-
src/app/test/client/page.tsx | 4 +-
src/app/test/server/page.tsx | 4 +-
src/components/LoginForm/index.tsx | 38 ++++++++++++-----
.../VerifyEmail/VerifyEmailSuccess.tsx | 41 +++++++++++++------
6 files changed, 81 insertions(+), 30 deletions(-)
diff --git a/next.config.mjs b/next.config.mjs
index ed9d5ee..26c10c7 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -1,7 +1,20 @@
import withBundleAnalyzer from "@next/bundle-analyzer";
/** @type {import('next').NextConfig} */
-const nextConfig = {};
+const nextConfig = {
+ /**
+ * The router cache is responsible for caching pages.
+ * This causes issues with your authentication state changes (like when you sign in).
+ * To fix this, we disable the router cache for dynamic routes.
+ * https://github.com/vercel/next.js/discussions/65487
+ * https://nextjs.org/docs/14/app/api-reference/next-config-js/staleTimes
+ */
+ experimental: {
+ staleTimes: {
+ dynamic: 0,
+ },
+ },
+};
const bundleAnalyzerConfig = withBundleAnalyzer({
enabled: process.env.ANALYZE === "true",
diff --git a/src/app/api/auth/[...nextauth]/options.ts b/src/app/api/auth/[...nextauth]/options.ts
index cc674fd..608b2a2 100644
--- a/src/app/api/auth/[...nextauth]/options.ts
+++ b/src/app/api/auth/[...nextauth]/options.ts
@@ -43,12 +43,18 @@ const authOptions: NextAuthOptions = {
}),
],
callbacks: {
- async jwt({ token, user }) {
+ async jwt({ token, user, trigger, session }) {
// the token gets the User object from the CredentialsProvider's authorize method
// but it only gets it the first time; every time after it is undefined
if (user) {
token.user = user;
}
+
+ // if you call the "update" function, refresh the user to reflect changes server-side
+ if (trigger === "update") {
+ token.user = session.user;
+ }
+
return token;
},
async session({ session, token }) {
@@ -60,7 +66,6 @@ const authOptions: NextAuthOptions = {
},
pages: {
signIn: "/auth/login",
- error: "/auth/login", // The login page will parse the error query parameter passed in
},
secret: process.env.NEXTAUTH_SECRET,
};
diff --git a/src/app/test/client/page.tsx b/src/app/test/client/page.tsx
index a20b3f6..a7acb6a 100644
--- a/src/app/test/client/page.tsx
+++ b/src/app/test/client/page.tsx
@@ -1,6 +1,6 @@
"use client";
-import { Box } from "@mui/material";
+import { Box, Typography } from "@mui/material";
export default function ClientComponentTestPage() {
return (
@@ -14,7 +14,7 @@ export default function ClientComponentTestPage() {
width: "100vw",
}}
>
-
Client component test page
+ Client component test page
);
}
diff --git a/src/app/test/server/page.tsx b/src/app/test/server/page.tsx
index d71c80a..ad9797d 100644
--- a/src/app/test/server/page.tsx
+++ b/src/app/test/server/page.tsx
@@ -1,4 +1,4 @@
-import { Box } from "@mui/material";
+import { Box, Typography } from "@mui/material";
export default function ServerComponentTestPage() {
return (
@@ -11,7 +11,7 @@ export default function ServerComponentTestPage() {
alignItems: "center",
}}
>
- Server component test page
+ Server component test page
);
}
diff --git a/src/components/LoginForm/index.tsx b/src/components/LoginForm/index.tsx
index a9e4abe..9eb97f7 100644
--- a/src/components/LoginForm/index.tsx
+++ b/src/components/LoginForm/index.tsx
@@ -4,9 +4,9 @@ import { zodResolver } from "@hookform/resolvers/zod";
import LoadingButton from "@mui/lab/LoadingButton";
import { Box, Skeleton, Typography } from "@mui/material";
import Link from "next/link";
-import { useSearchParams } from "next/navigation";
+import { useRouter, useSearchParams } from "next/navigation";
import { signIn } from "next-auth/react";
-import { Suspense, useEffect, useState } from "react";
+import { Suspense, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
@@ -28,6 +28,8 @@ type LoginFormValues = z.infer;
function LoginFormFields() {
const [isLoading, setIsLoading] = useState(false);
+ const router = useRouter();
+
const {
control,
handleSubmit,
@@ -40,17 +42,33 @@ function LoginFormFields() {
const searchParams = useSearchParams();
- useEffect(() => {
- const error = searchParams.get("error");
- if (error) {
- setError("root", { message: error });
- }
- }, [searchParams, setError]);
-
const onSubmit = async (data: LoginFormValues) => {
setIsLoading(true);
setError("root", { message: "" });
- await signIn("credentials", { ...data, callbackUrl: "/dashboard" });
+
+ const callbackUrl = searchParams.get("callbackUrl") ?? "/dashboard";
+
+ // redirect must be false, otherwise the callbackUrl will be lost
+ // if the user puts it invalid credentials
+ const signInResponse = await signIn("credentials", {
+ ...data,
+ callbackUrl: callbackUrl,
+ redirect: false,
+ });
+
+ if (!signInResponse) {
+ setError("root", { message: "An unknown error occurred" });
+ setIsLoading(false);
+ return;
+ }
+
+ if (signInResponse.error) {
+ setError("root", { message: signInResponse.error });
+ setIsLoading(false);
+ return;
+ }
+
+ router.push(callbackUrl);
};
return (
diff --git a/src/components/VerifyEmail/VerifyEmailSuccess.tsx b/src/components/VerifyEmail/VerifyEmailSuccess.tsx
index 560f49f..15b3340 100644
--- a/src/components/VerifyEmail/VerifyEmailSuccess.tsx
+++ b/src/components/VerifyEmail/VerifyEmailSuccess.tsx
@@ -3,8 +3,8 @@
import { Snackbar, Typography } from "@mui/material";
import { useRouter } from "next/navigation";
-import { signOut } from "next-auth/react";
-import { useEffect, useState } from "react";
+import { useSession } from "next-auth/react";
+import { useState } from "react";
import { verifyEmailWithToken } from "@/server/api/users/public-mutations";
import { EmailVerificationToken } from "@/types";
@@ -18,19 +18,34 @@ export default function VerifyEmailSuccess({
}: VerifyEmailSuccessProps) {
const [snackbarOpen, setSnackbarOpen] = useState(false);
const router = useRouter();
+ const { data: session, update } = useSession();
- useEffect(() => {
- const verifyEmail = async () => {
- await verifyEmailWithToken(emailVerificationToken.token);
- setSnackbarOpen(true);
- setTimeout(async () => {
- await signOut({ redirect: false, callbackUrl: "/" });
- router.push("/");
- }, 1000);
- };
+ const verifyEmail = async () => {
+ if (!session || session.user.isEmailVerified) {
+ return;
+ }
+ await verifyEmailWithToken(emailVerificationToken.token);
+ setSnackbarOpen(true);
+
+ // Update session to reflect database changes
+ await update({
+ ...session,
+ user: {
+ ...session?.user,
+ isEmailVerified: true,
+ },
+ });
+
+ setTimeout(() => {
+ router.push("/");
+ }, 2000);
+ };
+
+ // Call verifyEmail directly when session becomes available
+ if (session) {
void verifyEmail();
- }, []);
+ }
return (
<>
@@ -40,7 +55,7 @@ export default function VerifyEmailSuccess({
onClose={() => setSnackbarOpen(false)}
message="Email verified"
/>
- Verifying email...;
+ Verifying email...
>
);
}