finalized password reset + code refactoring
This commit is contained in:
parent
73dda21573
commit
329019b34e
|
@ -29,4 +29,6 @@ export default function useInitialData() {
|
|||
// setLinks();
|
||||
}
|
||||
}, [account]);
|
||||
|
||||
return status;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { ReactNode } from "react";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useSession } from "next-auth/react";
|
||||
import Loader from "../components/Loader";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import useInitialData from "@/hooks/useInitialData";
|
||||
import useAccountStore from "@/store/account";
|
||||
|
||||
|
@ -10,62 +9,68 @@ interface Props {
|
|||
children: ReactNode;
|
||||
}
|
||||
|
||||
const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER === "true";
|
||||
const stripeEnabled = process.env.NEXT_PUBLIC_STRIPE === "true";
|
||||
|
||||
export default function AuthRedirect({ children }: Props) {
|
||||
const router = useRouter();
|
||||
const { status, data } = useSession();
|
||||
const [redirect, setRedirect] = useState(true);
|
||||
const { status } = useSession();
|
||||
const [shouldRenderChildren, setShouldRenderChildren] = useState(false);
|
||||
const { account } = useAccountStore();
|
||||
|
||||
const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER === "true";
|
||||
const stripeEnabled = process.env.NEXT_PUBLIC_STRIPE === "true";
|
||||
|
||||
useInitialData();
|
||||
|
||||
useEffect(() => {
|
||||
if (!router.pathname.startsWith("/public")) {
|
||||
if (
|
||||
status === "authenticated" &&
|
||||
account.id &&
|
||||
!account.subscription?.active &&
|
||||
stripeEnabled
|
||||
) {
|
||||
router.push("/subscribe").then(() => {
|
||||
setRedirect(false);
|
||||
});
|
||||
const isLoggedIn = status === "authenticated";
|
||||
const isUnauthenticated = status === "unauthenticated";
|
||||
const isPublicPage = router.pathname.startsWith("/public");
|
||||
const hasInactiveSubscription =
|
||||
account.id && !account.subscription?.active && stripeEnabled;
|
||||
const routes = [
|
||||
{ path: "/login", isProtected: false },
|
||||
{ path: "/register", isProtected: false },
|
||||
{ path: "/confirmation", isProtected: false },
|
||||
{ path: "/forgot", isProtected: false },
|
||||
{ path: "/auth/reset-password", isProtected: false },
|
||||
{ path: "/", isProtected: false },
|
||||
{ path: "/subscribe", isProtected: true },
|
||||
{ path: "/dashboard", isProtected: true },
|
||||
{ path: "/settings", isProtected: true },
|
||||
{ path: "/collections", isProtected: true },
|
||||
{ path: "/links", isProtected: true },
|
||||
{ path: "/tags", isProtected: true },
|
||||
];
|
||||
|
||||
if (isPublicPage) {
|
||||
setShouldRenderChildren(true);
|
||||
} else {
|
||||
if (isLoggedIn && hasInactiveSubscription) {
|
||||
redirectTo("/subscribe");
|
||||
} else if (
|
||||
status === "authenticated" &&
|
||||
account.id &&
|
||||
(router.pathname === "/login" ||
|
||||
router.pathname === "/register" ||
|
||||
router.pathname === "/confirmation" ||
|
||||
router.pathname === "/subscribe" ||
|
||||
router.pathname === "/choose-username" ||
|
||||
router.pathname === "/forgot" ||
|
||||
router.pathname === "/")
|
||||
isLoggedIn &&
|
||||
!routes.some((e) => router.pathname.startsWith(e.path) && e.isProtected)
|
||||
) {
|
||||
router.push("/dashboard").then(() => {
|
||||
setRedirect(false);
|
||||
});
|
||||
redirectTo("/dashboard");
|
||||
} else if (
|
||||
status === "unauthenticated" &&
|
||||
!(
|
||||
router.pathname === "/login" ||
|
||||
router.pathname === "/register" ||
|
||||
router.pathname === "/confirmation" ||
|
||||
router.pathname === "/forgot"
|
||||
isUnauthenticated &&
|
||||
!routes.some(
|
||||
(e) => router.pathname.startsWith(e.path) && !e.isProtected
|
||||
)
|
||||
) {
|
||||
router.push("/login").then(() => {
|
||||
setRedirect(false);
|
||||
});
|
||||
} else if (status === "loading") setRedirect(true);
|
||||
else setRedirect(false);
|
||||
redirectTo("/login");
|
||||
} else {
|
||||
setRedirect(false);
|
||||
setShouldRenderChildren(true);
|
||||
}
|
||||
}
|
||||
}, [status, account, router.pathname]);
|
||||
|
||||
if (status !== "loading" && !redirect) return <>{children}</>;
|
||||
else return <></>;
|
||||
// return <>{children}</>;
|
||||
function redirectTo(destination: string) {
|
||||
router.push(destination).then(() => setShouldRenderChildren(true));
|
||||
}
|
||||
|
||||
if (status !== "loading" && shouldRenderChildren) {
|
||||
return <>{children}</>;
|
||||
} else {
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,11 +34,11 @@ export default async function sendPasswordResetRequest(
|
|||
address: process.env.EMAIL_FROM as string,
|
||||
},
|
||||
to: email,
|
||||
subject: "Verify your new Linkwarden email address",
|
||||
subject: "Linkwarden: Reset password instructions",
|
||||
html: emailTemplate({
|
||||
user,
|
||||
baseUrl: process.env.BASE_URL,
|
||||
url: `${process.env.BASE_URL}/auth/password-reset?token=${token}`,
|
||||
url: `${process.env.BASE_URL}/auth/reset-password?token=${token}`,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,12 +1,21 @@
|
|||
import { readFileSync } from "fs";
|
||||
import { SendVerificationRequestParams } from "next-auth/providers";
|
||||
import path from "path";
|
||||
import Handlebars from "handlebars";
|
||||
import transporter from "./transporter";
|
||||
|
||||
export default async function sendVerificationRequest(
|
||||
params: SendVerificationRequestParams
|
||||
) {
|
||||
type Params = {
|
||||
identifier: string;
|
||||
url: string;
|
||||
from: string;
|
||||
token: string;
|
||||
};
|
||||
|
||||
export default async function sendVerificationRequest({
|
||||
identifier,
|
||||
url,
|
||||
from,
|
||||
token,
|
||||
}: Params) {
|
||||
const emailsDir = path.resolve(process.cwd(), "templates");
|
||||
|
||||
const templateFile = readFileSync(
|
||||
|
@ -16,13 +25,12 @@ export default async function sendVerificationRequest(
|
|||
|
||||
const emailTemplate = Handlebars.compile(templateFile);
|
||||
|
||||
const { identifier, url, provider, token } = params;
|
||||
const { host } = new URL(url);
|
||||
const result = await transporter.sendMail({
|
||||
to: identifier,
|
||||
from: {
|
||||
name: "Linkwarden",
|
||||
address: provider.from as string,
|
||||
address: from as string,
|
||||
},
|
||||
subject: `Please verify your email address`,
|
||||
text: text({ url, host }),
|
||||
|
|
|
@ -9,6 +9,7 @@ import toast from "react-hot-toast";
|
|||
import { Toaster, ToastBar } from "react-hot-toast";
|
||||
import { Session } from "next-auth";
|
||||
import { isPWA } from "@/lib/client/utils";
|
||||
import useInitialData from "@/hooks/useInitialData";
|
||||
|
||||
export default function App({
|
||||
Component,
|
||||
|
@ -55,6 +56,7 @@ export default function App({
|
|||
<link rel="manifest" href="/site.webmanifest" />
|
||||
</Head>
|
||||
<AuthRedirect>
|
||||
{/* <GetData> */}
|
||||
<Toaster
|
||||
position="top-center"
|
||||
reverseOrder={false}
|
||||
|
@ -88,7 +90,17 @@ export default function App({
|
|||
)}
|
||||
</Toaster>
|
||||
<Component {...pageProps} />
|
||||
{/* </GetData> */}
|
||||
</AuthRedirect>
|
||||
</SessionProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// function GetData({ children }: { children: React.ReactNode }) {
|
||||
// const status = useInitialData();
|
||||
// return typeof window !== "undefined" && status !== "loading" ? (
|
||||
// children
|
||||
// ) : (
|
||||
// <></>
|
||||
// );
|
||||
// }
|
||||
|
|
|
@ -65,6 +65,7 @@ import ZohoProvider from "next-auth/providers/zoho";
|
|||
import ZoomProvider from "next-auth/providers/zoom";
|
||||
import * as process from "process";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { randomBytes } from "crypto";
|
||||
|
||||
const emailEnabled =
|
||||
process.env.EMAIL_FROM && process.env.EMAIL_SERVER ? true : false;
|
||||
|
@ -105,13 +106,54 @@ if (
|
|||
email: username?.toLowerCase(),
|
||||
},
|
||||
],
|
||||
emailVerified: { not: null },
|
||||
}
|
||||
: {
|
||||
username: username.toLowerCase(),
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) throw Error("Invalid credentials.");
|
||||
else if (!user?.emailVerified && emailEnabled) {
|
||||
const identifier = user?.email as string;
|
||||
const token = randomBytes(32).toString("hex");
|
||||
const url = `${
|
||||
process.env.NEXTAUTH_URL
|
||||
}/callback/email?token=${token}&email=${encodeURIComponent(
|
||||
identifier
|
||||
)}`;
|
||||
const from = process.env.EMAIL_FROM as string;
|
||||
|
||||
const recentVerificationRequestsCount =
|
||||
await prisma.verificationToken.count({
|
||||
where: {
|
||||
identifier,
|
||||
createdAt: {
|
||||
gt: new Date(new Date().getTime() - 1000 * 60 * 5), // 5 minutes
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (recentVerificationRequestsCount >= 4)
|
||||
throw Error("Too many requests. Please try again later.");
|
||||
|
||||
sendVerificationRequest({
|
||||
identifier,
|
||||
url,
|
||||
from,
|
||||
token,
|
||||
});
|
||||
|
||||
await prisma.verificationToken.create({
|
||||
data: {
|
||||
identifier,
|
||||
token,
|
||||
expires: new Date(Date.now() + 24 * 3600 * 1000), // 1 day
|
||||
},
|
||||
});
|
||||
|
||||
throw Error("Email not verified. Verification email sent.");
|
||||
}
|
||||
|
||||
let passwordMatches: boolean = false;
|
||||
|
||||
if (user?.password) {
|
||||
|
@ -120,7 +162,7 @@ if (
|
|||
|
||||
if (passwordMatches && user?.password) {
|
||||
return { id: user?.id };
|
||||
} else return null as any;
|
||||
} else throw Error("Invalid credentials.");
|
||||
},
|
||||
})
|
||||
);
|
||||
|
@ -132,8 +174,26 @@ if (emailEnabled) {
|
|||
server: process.env.EMAIL_SERVER,
|
||||
from: process.env.EMAIL_FROM,
|
||||
maxAge: 1200,
|
||||
sendVerificationRequest(params) {
|
||||
sendVerificationRequest(params);
|
||||
async sendVerificationRequest({ identifier, url, provider, token }) {
|
||||
const recentVerificationRequestsCount =
|
||||
await prisma.verificationToken.count({
|
||||
where: {
|
||||
identifier,
|
||||
createdAt: {
|
||||
gt: new Date(new Date().getTime() - 1000 * 60 * 5), // 5 minutes
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (recentVerificationRequestsCount >= 4)
|
||||
throw Error("Too many requests. Please try again later.");
|
||||
|
||||
sendVerificationRequest({
|
||||
identifier,
|
||||
url,
|
||||
from: provider.from as string,
|
||||
token,
|
||||
});
|
||||
},
|
||||
})
|
||||
);
|
||||
|
|
|
@ -74,7 +74,7 @@ export default async function resetPassword(
|
|||
});
|
||||
|
||||
return res.status(200).json({
|
||||
response: "Password reset successfully.",
|
||||
response: "Password has been reset successfully.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,26 +2,41 @@ import AccentSubmitButton from "@/components/AccentSubmitButton";
|
|||
import TextInput from "@/components/TextInput";
|
||||
import CenteredForm from "@/layouts/CenteredForm";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { FormEvent, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
|
||||
interface FormData {
|
||||
password: string;
|
||||
passwordConfirmation: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export default function ResetPassword() {
|
||||
const [submitLoader, setSubmitLoader] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const [form, setForm] = useState<FormData>({
|
||||
password: "",
|
||||
passwordConfirmation: "",
|
||||
token: router.query.token as string,
|
||||
});
|
||||
|
||||
const [isEmailSent, setIsEmailSent] = useState(false);
|
||||
const [requestSent, setRequestSent] = useState(false);
|
||||
|
||||
async function submitRequest() {
|
||||
const response = await fetch("/api/v1/auth/forgot-password", {
|
||||
async function submit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
|
||||
if (
|
||||
form.password !== "" &&
|
||||
form.token !== "" &&
|
||||
!requestSent &&
|
||||
!submitLoader
|
||||
) {
|
||||
setSubmitLoader(true);
|
||||
|
||||
const load = toast.loading("Sending password recovery link...");
|
||||
|
||||
const response = await fetch("/api/v1/auth/reset-password", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
@ -33,21 +48,10 @@ export default function ResetPassword() {
|
|||
|
||||
if (response.ok) {
|
||||
toast.success(data.response);
|
||||
setIsEmailSent(true);
|
||||
setRequestSent(true);
|
||||
} else {
|
||||
toast.error(data.response);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendConfirmation(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
|
||||
if (form.password !== "") {
|
||||
setSubmitLoader(true);
|
||||
|
||||
const load = toast.loading("Sending password recovery link...");
|
||||
|
||||
await submitRequest();
|
||||
|
||||
toast.dismiss(load);
|
||||
|
||||
|
@ -59,15 +63,15 @@ export default function ResetPassword() {
|
|||
|
||||
return (
|
||||
<CenteredForm>
|
||||
<form onSubmit={sendConfirmation}>
|
||||
<form onSubmit={submit}>
|
||||
<div className="p-4 mx-auto flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full bg-base-200 rounded-2xl shadow-md border border-neutral-content">
|
||||
<p className="text-3xl text-center font-extralight">
|
||||
{isEmailSent ? "Email Sent!" : "Forgot Password?"}
|
||||
{requestSent ? "Password Updated!" : "Reset Password"}
|
||||
</p>
|
||||
|
||||
<div className="divider my-0"></div>
|
||||
|
||||
{!isEmailSent ? (
|
||||
{!requestSent ? (
|
||||
<>
|
||||
<div>
|
||||
<p>
|
||||
|
@ -76,12 +80,12 @@ export default function ResetPassword() {
|
|||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm w-fit font-semibold mb-1">Email</p>
|
||||
<p className="text-sm w-fit font-semibold mb-1">New Password</p>
|
||||
|
||||
<TextInput
|
||||
autoFocus
|
||||
type="password"
|
||||
placeholder="johnny@example.com"
|
||||
placeholder="••••••••••••••"
|
||||
value={form.password}
|
||||
className="bg-base-100"
|
||||
onChange={(e) =>
|
||||
|
@ -92,23 +96,22 @@ export default function ResetPassword() {
|
|||
|
||||
<AccentSubmitButton
|
||||
type="submit"
|
||||
label="Send Login Link"
|
||||
label="Update Password"
|
||||
className="mt-2 w-full"
|
||||
loading={submitLoader}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-center">
|
||||
Check your email for a link to reset your password. If it doesn’t
|
||||
appear within a few minutes, check your spam folder.
|
||||
</p>
|
||||
)}
|
||||
<>
|
||||
<p>Your password has been successfully updated.</p>
|
||||
|
||||
<div className="flex items-baseline gap-1 justify-center">
|
||||
<Link href={"/login"} className="block font-bold">
|
||||
Go back
|
||||
<div className="mx-auto w-fit mt-3">
|
||||
<Link className="font-semibold" href="/login">
|
||||
Back to Login
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</CenteredForm>
|
||||
|
|
|
@ -1,8 +1,33 @@
|
|||
import CenteredForm from "@/layouts/CenteredForm";
|
||||
import { signIn } from "next-auth/react";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
export default function EmailConfirmaion() {
|
||||
const router = useRouter();
|
||||
|
||||
const [submitLoader, setSubmitLoader] = useState(false);
|
||||
|
||||
const resend = async () => {
|
||||
setSubmitLoader(true);
|
||||
|
||||
const load = toast.loading("Authenticating...");
|
||||
|
||||
const res = await signIn("email", {
|
||||
email: decodeURIComponent(router.query.email as string),
|
||||
callbackUrl: "/",
|
||||
redirect: false,
|
||||
});
|
||||
|
||||
toast.dismiss(load);
|
||||
|
||||
setSubmitLoader(false);
|
||||
|
||||
toast.success("Verification email sent.");
|
||||
};
|
||||
|
||||
return (
|
||||
<CenteredForm>
|
||||
<div className="p-4 max-w-[30rem] min-w-80 w-full rounded-2xl shadow-md mx-auto border border-neutral-content bg-base-200">
|
||||
|
@ -12,15 +37,16 @@ export default function EmailConfirmaion() {
|
|||
|
||||
<div className="divider my-3"></div>
|
||||
|
||||
<p>A sign in link has been sent to your email address.</p>
|
||||
|
||||
<p className="mt-3">
|
||||
Didn't see the email? Check your spam folder or visit the{" "}
|
||||
<Link href="/forgot" className="font-bold underline">
|
||||
Password Recovery
|
||||
</Link>{" "}
|
||||
page to resend the link.
|
||||
<p>
|
||||
A sign in link has been sent to your email address. If you don't see
|
||||
the email, check your spam folder.
|
||||
</p>
|
||||
|
||||
<div className="mx-auto w-fit mt-3">
|
||||
<div className="btn btn-ghost btn-sm" onClick={resend}>
|
||||
Resend Email
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CenteredForm>
|
||||
);
|
||||
|
|
|
@ -94,15 +94,15 @@ export default function Forgot() {
|
|||
/>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-center">
|
||||
<p>
|
||||
Check your email for a link to reset your password. If it doesn’t
|
||||
appear within a few minutes, check your spam folder.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-baseline gap-1 justify-center">
|
||||
<Link href={"/login"} className="block font-bold">
|
||||
Go back
|
||||
<div className="mx-auto w-fit mt-2">
|
||||
<Link className="font-semibold" href="/login">
|
||||
Back to Login
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -47,7 +47,7 @@ export default function Login({
|
|||
setSubmitLoader(false);
|
||||
|
||||
if (!res?.ok) {
|
||||
toast.error("Invalid login.");
|
||||
toast.error(res?.error || "Invalid credentials.");
|
||||
}
|
||||
} else {
|
||||
toast.error("Please fill out all the fields.");
|
||||
|
@ -108,7 +108,7 @@ export default function Login({
|
|||
<div className="w-fit ml-auto mt-1">
|
||||
<Link
|
||||
href={"/forgot"}
|
||||
className="text-gray-500 dark:text-gray-400 font-semibold"
|
||||
className="text-neutral font-semibold"
|
||||
data-testid="forgot-password-link"
|
||||
>
|
||||
Forgot Password?
|
||||
|
@ -158,7 +158,7 @@ export default function Login({
|
|||
<p className="w-fit text-gray-500 dark:text-gray-400">New here?</p>
|
||||
<Link
|
||||
href={"/register"}
|
||||
className="block text-black dark:text-white font-semibold"
|
||||
className="font-semibold"
|
||||
data-testid="register-link"
|
||||
>
|
||||
Sign Up
|
||||
|
|
|
@ -76,12 +76,17 @@ export default function Register() {
|
|||
setSubmitLoader(false);
|
||||
|
||||
if (response.ok) {
|
||||
if (form.email && emailEnabled)
|
||||
if (form.email && emailEnabled) {
|
||||
await signIn("email", {
|
||||
email: form.email,
|
||||
callbackUrl: "/",
|
||||
redirect: false,
|
||||
});
|
||||
else if (!emailEnabled) router.push("/login");
|
||||
|
||||
router.push(
|
||||
"/confirmation?email=" + encodeURIComponent(form.email)
|
||||
);
|
||||
} else if (!emailEnabled) router.push("/login");
|
||||
|
||||
toast.success("User Created!");
|
||||
} else {
|
||||
|
|
Ŝarĝante…
Reference in New Issue