Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/pr_preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ jobs:
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const {DEPLOYMENT_URL, DEPLOY_STATUS, RUN_URL} = process.env;
const {DEPLOYMENT_URL, DEPLOY_STATUS, RUN_URL, PR_NUM} = process.env;
const status = DEPLOY_STATUS === "success" ? "✅ Ready" : "❌ Failed to deploy";

const body = `### 🚀 Preview Deployment:
Expand Down
21,624 changes: 12,501 additions & 9,123 deletions package-lock.json

Large diffs are not rendered by default.

110 changes: 110 additions & 0 deletions src/app/reset-password/SendResetLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { Button } from "@/src/components/ui/button";
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyTitle,
} from "@/src/components/ui/empty";
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from "@/src/components/ui/form";
import { Input } from "@/src/components/ui/input";
import { authClient } from "@/src/lib/auth-client";
import { zodResolver } from "@hookform/resolvers/zod";
import Link from "next/link";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import z from "zod";

export default function SendResetLink() {
const [isLoading, setIsLoading] = useState(false);
const formSchema = z.object({
email: z.email({ error: "Invalid email address" }),
});

const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
email: "",
},
});

const handleSendResetLink = async (email: string) => {
setIsLoading(true);
const toastId = toast.loading("Sending password reset link...");
const { error } = await authClient.requestPasswordReset({
email,
redirectTo: "/reset-password",
});

if (error) {
toast.error(error.message, { id: toastId });
} else {
toast.success(
"Successfully sent password reset link! Please check your inbox.",
{ id: toastId },
);
}
setIsLoading(false);
};

return (
<div className="flex min-h-dvh justify-center items-center">
<Empty className="w-full p-10">
<EmptyHeader>
<EmptyTitle>Reset your password</EmptyTitle>
<EmptyDescription>
We will send you a link to reset your password
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Form {...form}>
<form
className="w-full flex flex-col gap-4"
onSubmit={form.handleSubmit((data) =>
handleSendResetLink(data.email),
)}
>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
placeholder="Email"
className="h-13 w-full"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
className="h-13 w-full"
disabled={isLoading || !form.formState.isValid}
>
Send Reset Link
</Button>
<Button
className="h-13 w-full"
variant={"outline"}
type="button"
asChild
>
<Link href={"/sign-in"}>Back To Login</Link>
</Button>
</form>
</Form>
</EmptyContent>
</Empty>
</div>
);
}
135 changes: 135 additions & 0 deletions src/app/reset-password/SetNewPassword.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { Button } from "@/src/components/ui/button";
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyTitle,
} from "@/src/components/ui/empty";
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from "@/src/components/ui/form";
import { Input } from "@/src/components/ui/input";
import { authClient } from "@/src/lib/auth-client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import z from "zod";

interface SetNewPasswordProps {
token: string;
}
export default function SetNewPassword({ token }: SetNewPasswordProps) {
const [isLoading, setIsLoading] = useState(false);
const router = useRouter();

const formSchema = z
.object({
newPassword: z
.string()
.min(6, { error: "Password must be at least 6 characters" })
.max(256, { error: "Password must not have more than 256 characters" }),

confirmPassword: z.string(),
})
.refine((data) => data.newPassword === data.confirmPassword, {
error: "Passwords do not match",
path: ["confirmPassword"],
});

const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
newPassword: "",
confirmPassword: "",
},
});

const handlePasswordReset = async (newPassword: string) => {
setIsLoading(true);
const toastId = toast.loading("Resetting your password...");
const { error } = await authClient.resetPassword({
newPassword,
token,
});

if (error) {
toast.error(error.message, { id: toastId });
} else {
toast.success("Successfully reset your password!", { id: toastId });
}

setIsLoading(false);
router.push("/sign-in");
};

return (
<div className="flex min-h-dvh justify-center items-center">
<Empty className="w-full p-10">
<EmptyHeader>
<EmptyTitle>Reset your password</EmptyTitle>
<EmptyDescription>
Enter your new password below
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Form {...form}>
<form
className="w-full flex flex-col gap-4"
onSubmit={form.handleSubmit((data) =>
handlePasswordReset(data.newPassword),
)}
>
<FormField
control={form.control}
name="newPassword"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
placeholder="New Password"
type="password"
className="h-13 w-full"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
placeholder="Confirm Password"
type="password"
className="h-13 w-full"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
className="h-13 w-full"
disabled={isLoading || !form.formState.isValid}
>
Reset Password
</Button>
</form>
</Form>
</EmptyContent>
</Empty>
</div>
);
}
10 changes: 10 additions & 0 deletions src/app/reset-password/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import ProtectedRoute from "@/src/components/ProtectedRoute";
import React from "react";

export default function Layout({ children }: React.PropsWithChildren) {
return (
<ProtectedRoute rules={["requireLoggedOff"]} fallbackRoute="/dashboard">
{children}
</ProtectedRoute>
);
}
29 changes: 29 additions & 0 deletions src/app/reset-password/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"use client";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect } from "react";
import { toast } from "sonner";
import SendResetLink from "./SendResetLink";
import SetNewPassword from "./SetNewPassword";

export default function Page() {
const searchParams = useSearchParams();
const router = useRouter();

const token = searchParams.get("token");
const error = searchParams.get("error");

useEffect(() => {
if (error) {
setTimeout(() => {
toast.error("There was an error while loading reset password form");
router.replace("/reset-password");
}, 100);
}
}, [error, router]);

if (token) {
return <SetNewPassword token={token} />;
} else {
return <SendResetLink />;
}
}
4 changes: 3 additions & 1 deletion src/app/sign-in/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,10 @@ export default function LoginCard() {
<Button
className="text-muted-foreground h-0 font-semibold"
variant={"link"}
type="button"
asChild
>
<Link href={"/forgot"}>Forgot password?</Link>
<Link href={"/reset-password"}>Forgot password?</Link>
</Button>
<Button className="h-12 w-full" disabled={isLoading}>
Login
Expand All @@ -150,6 +151,7 @@ export default function LoginCard() {
<Button
className="text-blue-500 p-0 h-0 font-semibold"
variant={"link"}
type="button"
asChild
>
<Link href={"/sign-up"}>Sign Up</Link>
Expand Down
7 changes: 7 additions & 0 deletions src/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ export const auth = betterAuth({
minPasswordLength: 6,
maxPasswordLength: 256,
autoSignIn: true,
sendResetPassword: async ({ user, url }) => {
await emailTransporter.sendMail({
to: user.email,
subject: "Reset your password",
text: `Click the link to reset your password: ${url}`,
});
},
},
emailVerification: {
sendOnSignUp: true,
Expand Down