feat: enhance authentication flow with improved email verification and password reset
This commit is contained in:
parent
ecaf761b61
commit
940b8e9dfc
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "mksaas-demo",
|
||||
"name": "mksaas-template",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -1,15 +0,0 @@
|
||||
import { NewVerificationForm } from "@/components/auth/new-verification-form";
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { constructMetadata } from "@/lib/metadata";
|
||||
|
||||
export const metadata = constructMetadata({
|
||||
title: "New Verification",
|
||||
description: "New Verification",
|
||||
canonicalUrl: `${siteConfig.url}/auth/new-verification`,
|
||||
});
|
||||
|
||||
const NewVerificationPage = () => {
|
||||
return <NewVerificationForm />;
|
||||
};
|
||||
|
||||
export default NewVerificationPage;
|
@ -1,4 +1,4 @@
|
||||
import { ResetForm } from "@/components/auth/reset-form";
|
||||
import { ResetPasswordForm } from "@/components/auth/reset-form";
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { constructMetadata } from "@/lib/metadata";
|
||||
|
||||
@ -9,7 +9,7 @@ export const metadata = constructMetadata({
|
||||
});
|
||||
|
||||
const ResetPage = () => {
|
||||
return <ResetForm />;
|
||||
return <ResetPasswordForm />;
|
||||
};
|
||||
|
||||
export default ResetPage;
|
@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
// import { login } from "@/actions/login";
|
||||
import { AuthCard } from "@/components/auth/auth-card";
|
||||
import { Icons } from "@/components/icons/icons";
|
||||
import { FormError } from "@/components/shared/form-error";
|
||||
@ -15,11 +14,12 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { LoginSchema } from "@/lib/schemas";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useState, useTransition } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import type * as z from "zod";
|
||||
@ -32,9 +32,11 @@ export const LoginForm = ({ className }: { className?: string }) => {
|
||||
? "Email already in use with different provider!"
|
||||
: "";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const [error, setError] = useState<string | undefined>("");
|
||||
const [success, setSuccess] = useState<string | undefined>("");
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
|
||||
const form = useForm<z.infer<typeof LoginSchema>>({
|
||||
resolver: zodResolver(LoginSchema),
|
||||
@ -44,36 +46,35 @@ export const LoginForm = ({ className }: { className?: string }) => {
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (values: z.infer<typeof LoginSchema>) => {
|
||||
setError("");
|
||||
setSuccess("");
|
||||
|
||||
// startTransition(() => {
|
||||
// login(values, callbackUrl)
|
||||
// .then((data) => {
|
||||
// // console.log('login, data:', data);
|
||||
// if (data?.status === "error") {
|
||||
// console.log("login, error:", data.message);
|
||||
// form.reset();
|
||||
// setError(data.message);
|
||||
// }
|
||||
|
||||
// if (data?.status === "success") {
|
||||
// console.log("login, success:", data.message);
|
||||
// form.reset();
|
||||
// setSuccess(data.message);
|
||||
|
||||
// // if success without redirect url, means sent confirmation email
|
||||
// if (data.redirectUrl) {
|
||||
// window.location.href = data.redirectUrl;
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// .catch((error) => {
|
||||
// console.log("login, error:", error);
|
||||
// setError("Something went wrong");
|
||||
// });
|
||||
// });
|
||||
const onSubmit = async (values: z.infer<typeof LoginSchema>) => {
|
||||
// 1. if callbackUrl is provided, user will be redirected to the callbackURL after login successfully.
|
||||
// if user email is not verified, a new verification email will be sent to the user with the callbackURL.
|
||||
// 2. if callbackUrl is not provided, we should redirect manually in the onSuccess callback.
|
||||
const { data, error } = await authClient.signIn.email({
|
||||
email: values.email,
|
||||
password: values.password,
|
||||
callbackURL: callbackUrl || "/dashboard",
|
||||
}, {
|
||||
onRequest: (ctx) => {
|
||||
// console.log("login, request:", ctx.url);
|
||||
setIsPending(true);
|
||||
setError("");
|
||||
setSuccess("");
|
||||
},
|
||||
onResponse: (ctx) => {
|
||||
// console.log("login, response:", ctx.response);
|
||||
setIsPending(false);
|
||||
},
|
||||
onSuccess: (ctx) => {
|
||||
// console.log("login, success:", ctx.data);
|
||||
// setSuccess("Login successful");
|
||||
// router.push(callbackUrl || "/dashboard");
|
||||
},
|
||||
onError: (ctx) => {
|
||||
console.log("login, error:", ctx.error);
|
||||
setError(ctx.error.message);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
@ -118,7 +119,7 @@ export const LoginForm = ({ className }: { className?: string }) => {
|
||||
asChild
|
||||
className="px-0 font-normal text-muted-foreground"
|
||||
>
|
||||
<Link href="/auth/reset" className="text-xs underline">
|
||||
<Link href="/auth/reset-password" className="text-xs underline">
|
||||
Forgot password?
|
||||
</Link>
|
||||
</Button>
|
||||
|
@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
// import { newPassword } from "@/actions/new-password";
|
||||
import { AuthCard } from "@/components/auth/auth-card";
|
||||
import { Icons } from "@/components/icons/icons";
|
||||
import { FormError } from "@/components/shared/form-error";
|
||||
@ -15,9 +14,10 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { NewPasswordSchema } from "@/lib/schemas";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useState, useTransition } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import type * as z from "zod";
|
||||
@ -25,10 +25,16 @@ import type * as z from "zod";
|
||||
export const NewPasswordForm = () => {
|
||||
const searchParams = useSearchParams();
|
||||
const token = searchParams.get("token");
|
||||
if (!token) {
|
||||
// TODO: Handle the error
|
||||
return <div>Invalid token</div>;
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const [error, setError] = useState<string | undefined>("");
|
||||
const [success, setSuccess] = useState<string | undefined>("");
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
|
||||
const form = useForm<z.infer<typeof NewPasswordSchema>>({
|
||||
resolver: zodResolver(NewPasswordSchema),
|
||||
@ -37,28 +43,31 @@ export const NewPasswordForm = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (values: z.infer<typeof NewPasswordSchema>) => {
|
||||
setError("");
|
||||
setSuccess("");
|
||||
|
||||
// startTransition(() => {
|
||||
// newPassword(values, token)
|
||||
// .then((data) => {
|
||||
// if (data?.status === "error") {
|
||||
// console.log("newPassword, error:", data.message);
|
||||
// setError(data.message);
|
||||
// }
|
||||
|
||||
// if (data?.status === "success") {
|
||||
// console.log("newPassword, success:", data.message);
|
||||
// setSuccess(data.message);
|
||||
// }
|
||||
// })
|
||||
// .catch(() => {
|
||||
// console.log("newPassword, error:", error);
|
||||
// setError("Something went wrong");
|
||||
// });
|
||||
// });
|
||||
const onSubmit = async (values: z.infer<typeof NewPasswordSchema>) => {
|
||||
const { data, error } = await authClient.resetPassword({
|
||||
newPassword: values.password,
|
||||
token,
|
||||
}, {
|
||||
onRequest: (ctx) => {
|
||||
// console.log("resetPassword, request:", ctx.url);
|
||||
setIsPending(true);
|
||||
setError("");
|
||||
setSuccess("");
|
||||
},
|
||||
onResponse: (ctx) => {
|
||||
// console.log("resetPassword, response:", ctx.response);
|
||||
setIsPending(false);
|
||||
},
|
||||
onSuccess: (ctx) => {
|
||||
// console.log("resetPassword, success:", ctx.data);
|
||||
// setSuccess("Password reset successfully");
|
||||
router.push("/auth/login");
|
||||
},
|
||||
onError: (ctx) => {
|
||||
console.log("resetPassword, error:", ctx.error);
|
||||
setError(ctx.error.message);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -1,62 +0,0 @@
|
||||
"use client";
|
||||
|
||||
// import { newVerification } from "@/actions/new-verification";
|
||||
import { AuthCard } from "@/components/auth/auth-card";
|
||||
import { Icons } from "@/components/icons/icons";
|
||||
import { FormError } from "@/components/shared/form-error";
|
||||
import { FormSuccess } from "@/components/shared/form-success";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
export const NewVerificationForm = () => {
|
||||
const [error, setError] = useState<string | undefined>();
|
||||
const [success, setSuccess] = useState<string | undefined>();
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
const token = searchParams.get("token");
|
||||
|
||||
const onSubmit = useCallback(() => {
|
||||
if (success || error) return;
|
||||
|
||||
if (!token) {
|
||||
setError("Missing token!");
|
||||
return;
|
||||
}
|
||||
|
||||
// newVerification(token)
|
||||
// .then((data) => {
|
||||
// if (data.status === "success") {
|
||||
// setSuccess(data.message);
|
||||
// console.log("newVerification, success:", data.message);
|
||||
// }
|
||||
// if (data.status === "error") {
|
||||
// setError(data.message);
|
||||
// console.log("newVerification, error:", data.message);
|
||||
// }
|
||||
// })
|
||||
// .catch(() => {
|
||||
// setError("Something went wrong");
|
||||
// });
|
||||
}, [token, success, error]);
|
||||
|
||||
useEffect(() => {
|
||||
onSubmit();
|
||||
}, [onSubmit]);
|
||||
|
||||
return (
|
||||
<AuthCard
|
||||
headerLabel="Confirming your verification"
|
||||
bottomButtonLabel="Back to login"
|
||||
bottomButtonHref="/auth/login"
|
||||
className="border-none"
|
||||
>
|
||||
<div className="flex items-center w-full justify-center">
|
||||
{!success && !error && (
|
||||
<Icons.spinner className="w-4 h-4 animate-spin" />
|
||||
)}
|
||||
<FormSuccess message={success} />
|
||||
{!success && <FormError message={error} />}
|
||||
</div>
|
||||
</AuthCard>
|
||||
);
|
||||
};
|
@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
// import { register } from "@/actions/register";
|
||||
import { AuthCard } from "@/components/auth/auth-card";
|
||||
import { Icons } from "@/components/icons/icons";
|
||||
import { FormError } from "@/components/shared/form-error";
|
||||
@ -15,8 +14,10 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { RegisterSchema } from "@/lib/schemas";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState, useTransition } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import type * as z from "zod";
|
||||
@ -24,7 +25,8 @@ import type * as z from "zod";
|
||||
export const RegisterForm = () => {
|
||||
const [error, setError] = useState<string | undefined>("");
|
||||
const [success, setSuccess] = useState<string | undefined>("");
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const form = useForm<z.infer<typeof RegisterSchema>>({
|
||||
resolver: zodResolver(RegisterSchema),
|
||||
@ -35,27 +37,38 @@ export const RegisterForm = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (values: z.infer<typeof RegisterSchema>) => {
|
||||
setError("");
|
||||
setSuccess("");
|
||||
|
||||
// startTransition(() => {
|
||||
// register(values)
|
||||
// .then((data) => {
|
||||
// if (data.status === "error") {
|
||||
// console.log("register, error:", data.message);
|
||||
// setError(data.message);
|
||||
// }
|
||||
// if (data.status === "success") {
|
||||
// console.log("register, success:", data.message);
|
||||
// setSuccess(data.message);
|
||||
// }
|
||||
// })
|
||||
// .catch((error) => {
|
||||
// console.log("register, error:", error);
|
||||
// setError("Something went wrong");
|
||||
// });
|
||||
// });
|
||||
const onSubmit = async (values: z.infer<typeof RegisterSchema>) => {
|
||||
// 1. if requireEmailVerification is true, callbackURL will be used in the verification email,
|
||||
// the user will be redirected to the callbackURL after the email is verified.
|
||||
// 2. if requireEmailVerification is false, the user will not be redirected to the callbackURL,
|
||||
// we should redirect to the callbackURL manually in the onSuccess callback.
|
||||
const { data, error } = await authClient.signUp.email({
|
||||
email: values.email,
|
||||
password: values.password,
|
||||
name: values.name,
|
||||
callbackURL: "/dashboard",
|
||||
}, {
|
||||
onRequest: (ctx) => {
|
||||
console.log("register, request:", ctx.url);
|
||||
setIsPending(true);
|
||||
setError("");
|
||||
setSuccess("");
|
||||
},
|
||||
onResponse: (ctx) => {
|
||||
console.log("register, response:", ctx.response);
|
||||
setIsPending(false);
|
||||
},
|
||||
onSuccess: (ctx) => {
|
||||
// sign up success, user information stored in ctx.data
|
||||
// console.log("register, success:", ctx.data);
|
||||
setSuccess('Please check your email for verification');
|
||||
},
|
||||
onError: (ctx) => {
|
||||
// sign up fail, display the error message
|
||||
console.log("register, error:", ctx.error.message);
|
||||
setError(ctx.error.message);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
// import { reset } from "@/actions/reset";
|
||||
import { AuthCard } from "@/components/auth/auth-card";
|
||||
import { FormError } from "@/components/shared/form-error";
|
||||
import { FormSuccess } from "@/components/shared/form-success";
|
||||
@ -14,46 +13,50 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ResetSchema } from "@/lib/schemas";
|
||||
import { ResetPasswordSchema } from "@/lib/schemas";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useState, useTransition } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import type * as z from "zod";
|
||||
import { Icons } from "../icons/icons";
|
||||
import { Icons } from "@/components/icons/icons";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
|
||||
export const ResetForm = () => {
|
||||
export const ResetPasswordForm = () => {
|
||||
const [error, setError] = useState<string | undefined>("");
|
||||
const [success, setSuccess] = useState<string | undefined>("");
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
|
||||
const form = useForm<z.infer<typeof ResetSchema>>({
|
||||
resolver: zodResolver(ResetSchema),
|
||||
const form = useForm<z.infer<typeof ResetPasswordSchema>>({
|
||||
resolver: zodResolver(ResetPasswordSchema),
|
||||
defaultValues: {
|
||||
email: "",
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (values: z.infer<typeof ResetSchema>) => {
|
||||
setError("");
|
||||
setSuccess("");
|
||||
|
||||
// startTransition(() => {
|
||||
// reset(values)
|
||||
// .then((data) => {
|
||||
// if (data.status === "error") {
|
||||
// console.log("reset, error:", data.message);
|
||||
// setError(data.message);
|
||||
// }
|
||||
// if (data.status === "success") {
|
||||
// console.log("reset, success:", data.message);
|
||||
// setSuccess(data.message);
|
||||
// }
|
||||
// })
|
||||
// .catch((error) => {
|
||||
// console.log("reset, error:", error);
|
||||
// setError("Something went wrong");
|
||||
// });
|
||||
// });
|
||||
const onSubmit = async (values: z.infer<typeof ResetPasswordSchema>) => {
|
||||
const { data, error } = await authClient.forgetPassword({
|
||||
email: values.email,
|
||||
redirectTo: "/auth/new-password",
|
||||
}, {
|
||||
onRequest: (ctx) => {
|
||||
// console.log("reset, request:", ctx.url);
|
||||
setIsPending(true);
|
||||
setError("");
|
||||
setSuccess("");
|
||||
},
|
||||
onResponse: (ctx) => {
|
||||
// console.log("reset, response:", ctx.response);
|
||||
setIsPending(false);
|
||||
},
|
||||
onSuccess: (ctx) => {
|
||||
// console.log("reset, success:", ctx.data);
|
||||
setSuccess("Please check your email for the reset password link");
|
||||
},
|
||||
onError: (ctx) => {
|
||||
console.log("reset, error:", ctx.error);
|
||||
setError(ctx.error.message);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -2,8 +2,13 @@ import { betterAuth } from "better-auth";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import db from "@/db/index";
|
||||
import { user, session, account, verification } from "@/db/schema";
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { resend } from "@/lib/email/resend";
|
||||
|
||||
const from = process.env.BETTER_AUTH_EMAIL || "delivered@resend.dev";
|
||||
|
||||
export const auth = betterAuth({
|
||||
appName: siteConfig.name,
|
||||
database: drizzleAdapter(db, {
|
||||
provider: "pg", // or "mysql", "sqlite"
|
||||
// The schema object that defines the tables and fields
|
||||
@ -17,7 +22,28 @@ export const auth = betterAuth({
|
||||
},
|
||||
}),
|
||||
emailAndPassword: {
|
||||
enabled: true
|
||||
enabled: true,
|
||||
requireEmailVerification: true,
|
||||
// https://www.better-auth.com/docs/authentication/email-password#forget-password
|
||||
async sendResetPassword({ user, url }) {
|
||||
await resend.emails.send({
|
||||
from,
|
||||
to: user.email,
|
||||
subject: "Reset your password",
|
||||
react: `Click the link to reset your password: ${url}`,
|
||||
});
|
||||
},
|
||||
},
|
||||
emailVerification: {
|
||||
// https://www.better-auth.com/docs/authentication/email-password#require-email-verification
|
||||
sendVerificationEmail: async ( { user, url, token }, request) => {
|
||||
await resend.emails.send({
|
||||
from,
|
||||
to: user.email,
|
||||
subject: "Verify your email address",
|
||||
text: `Click the link to verify your email: ${url}`,
|
||||
});
|
||||
},
|
||||
},
|
||||
socialProviders: {
|
||||
github: {
|
||||
|
3
src/lib/email/resend.ts
Normal file
3
src/lib/email/resend.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { Resend } from "resend";
|
||||
|
||||
export const resend = new Resend(process.env.RESEND_API_KEY);
|
@ -103,14 +103,14 @@ export type UserLinkData = z.infer<typeof UserLinkSchema>;
|
||||
|
||||
export const UserPasswordSchema = z
|
||||
.object({
|
||||
password: z.string().min(6, {
|
||||
message: "Minimum of 6 characters required",
|
||||
password: z.string().min(8, {
|
||||
message: "Minimum 8 characters required",
|
||||
}),
|
||||
newPassword: z.string().min(6, {
|
||||
message: "Minimum of 6 characters required",
|
||||
newPassword: z.string().min(8, {
|
||||
message: "Minimum 8 characters required",
|
||||
}),
|
||||
confirmPassword: z.string().min(6, {
|
||||
message: "Minimum of 6 characters required",
|
||||
confirmPassword: z.string().min(8, {
|
||||
message: "Minimum 8 characters required",
|
||||
}),
|
||||
})
|
||||
.refine(
|
||||
@ -159,12 +159,12 @@ export type UserPasswordData = z.infer<typeof UserPasswordSchema>;
|
||||
* auth related schemas
|
||||
*/
|
||||
export const NewPasswordSchema = z.object({
|
||||
password: z.string().min(6, {
|
||||
message: "Minimum of 6 characters required",
|
||||
password: z.string().min(8, {
|
||||
message: "Minimum 8 characters required",
|
||||
}),
|
||||
});
|
||||
|
||||
export const ResetSchema = z.object({
|
||||
export const ResetPasswordSchema = z.object({
|
||||
email: z.string().email({
|
||||
message: "Email is required",
|
||||
}),
|
||||
@ -183,8 +183,8 @@ export const RegisterSchema = z.object({
|
||||
email: z.string().email({
|
||||
message: "Email is required",
|
||||
}),
|
||||
password: z.string().min(6, {
|
||||
message: "Minimum 6 characters required",
|
||||
password: z.string().min(8, {
|
||||
message: "Minimum 8 characters required",
|
||||
}),
|
||||
name: z.string().min(1, {
|
||||
message: "Name is required",
|
||||
|
Loading…
Reference in New Issue
Block a user