From 2ccbd61a31e5d828f52e8e3366468b142b85c157 Mon Sep 17 00:00:00 2001
From: femyeda
Date: Wed, 23 Jun 2021 16:11:38 -0500
Subject: [PATCH 01/10] dep: handlebars
---
package.json | 1 +
yarn.lock | 29 ++++++++++++++++++++++++++++-
2 files changed, 29 insertions(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 31e86ca4..af11fce6 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"bcryptjs": "^2.4.3",
"dayjs": "^1.10.4",
"googleapis": "^67.1.1",
+ "handlebars": "^4.7.7",
"ics": "^2.27.0",
"lodash.merge": "^4.6.2",
"next": "^10.2.0",
diff --git a/yarn.lock b/yarn.lock
index fd8a34d2..17d976cf 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1996,6 +1996,18 @@ gtoken@^5.0.4:
google-p12-pem "^3.0.3"
jws "^4.0.0"
+handlebars@^4.7.7:
+ version "4.7.7"
+ resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1"
+ integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==
+ dependencies:
+ minimist "^1.2.5"
+ neo-async "^2.6.0"
+ source-map "^0.6.1"
+ wordwrap "^1.0.0"
+ optionalDependencies:
+ uglify-js "^3.1.4"
+
has-ansi@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
@@ -2789,7 +2801,7 @@ minimatch@^3.0.4:
dependencies:
brace-expansion "^1.1.7"
-minimist@^1.1.1, minimist@^1.2.0:
+minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
@@ -2850,6 +2862,11 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
+neo-async@^2.6.0:
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
+ integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
+
next-auth@^3.13.2:
version "3.19.8"
resolved "https://registry.yarnpkg.com/next-auth/-/next-auth-3.19.8.tgz#32331f33dd73b46ec5c774735a9db78f9dbba3c7"
@@ -4342,6 +4359,11 @@ typescript@^4.2.3:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.4.tgz#8610b59747de028fda898a8aef0e103f156d0961"
integrity sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==
+uglify-js@^3.1.4:
+ version "3.13.9"
+ resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.9.tgz#4d8d21dcd497f29cfd8e9378b9df123ad025999b"
+ integrity sha512-wZbyTQ1w6Y7fHdt8sJnHfSIuWeDgk6B5rCb4E/AM6QNNPbOMIZph21PW5dRB3h7Df0GszN+t7RuUH6sWK5bF0g==
+
unbox-primitive@^1.0.0, unbox-primitive@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471"
@@ -4498,6 +4520,11 @@ word-wrap@^1.2.3:
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
+wordwrap@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
+ integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=
+
wrap-ansi@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
From 173d4cda773b34dd653afc0d40c90b88b05281db Mon Sep 17 00:00:00 2001
From: femyeda
Date: Wed, 23 Jun 2021 16:16:41 -0500
Subject: [PATCH 02/10] dep: nodemailer types
---
package.json | 1 +
yarn.lock | 7 +++++++
2 files changed, 8 insertions(+)
diff --git a/package.json b/package.json
index af11fce6..2bec1c87 100644
--- a/package.json
+++ b/package.json
@@ -38,6 +38,7 @@
},
"devDependencies": {
"@types/node": "^14.14.33",
+ "@types/nodemailer": "^6.4.2",
"@types/react": "^17.0.3",
"@typescript-eslint/eslint-plugin": "^4.27.0",
"@typescript-eslint/parser": "^4.27.0",
diff --git a/yarn.lock b/yarn.lock
index 17d976cf..c2f035e9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -341,6 +341,13 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.44.tgz#df7503e6002847b834371c004b372529f3f85215"
integrity sha512-+gaugz6Oce6ZInfI/tK4Pq5wIIkJMEJUu92RB3Eu93mtj4wjjjz9EB5mLp5s1pSsLXdC/CPut/xF20ZzAQJbTA==
+"@types/nodemailer@^6.4.2":
+ version "6.4.2"
+ resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.2.tgz#d8ee254c969e6ad83fb9a0a0df3a817406a3fa3b"
+ integrity sha512-yhsqg5Xbr8aWdwjFS3QjkniW5/tLpWXtOYQcJdo9qE3DolBxsKzgRCQrteaMY0hos8MklJNSEsMqDpZynGzMNg==
+ dependencies:
+ "@types/node" "*"
+
"@types/parse-json@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
From a1ddb873f0ae338e77b951c0b533c5efc0af6ecc Mon Sep 17 00:00:00 2001
From: Peer Richelsen
Date: Wed, 23 Jun 2021 23:45:07 +0100
Subject: [PATCH 03/10] minor change to reschedule info in email
---
lib/emails/EventMail.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/lib/emails/EventMail.ts b/lib/emails/EventMail.ts
index 2d4d8489..b76bb5a1 100644
--- a/lib/emails/EventMail.ts
+++ b/lib/emails/EventMail.ts
@@ -127,7 +127,8 @@ export default abstract class EventMail {
protected getAdditionalFooter(): string {
return `
- Need to change this event?
+
+ Need to change this event?
Cancel: ${this.getCancelLink()}
Reschedule: ${this.getRescheduleLink()}
`;
From ea5692c20cfd65d81c09b5dcc4ca7721a30610ab Mon Sep 17 00:00:00 2001
From: Bailey Pumfleet
Date: Thu, 24 Jun 2021 14:36:31 +0100
Subject: [PATCH 04/10] Add bookings page
---
components/Shell.tsx | 13 ++++-
pages/bookings/index.tsx | 121 +++++++++++++++++++++++++++++++++++++++
2 files changed, 131 insertions(+), 3 deletions(-)
create mode 100644 pages/bookings/index.tsx
diff --git a/components/Shell.tsx b/components/Shell.tsx
index e51ff814..57db5acc 100644
--- a/components/Shell.tsx
+++ b/components/Shell.tsx
@@ -59,9 +59,16 @@ export default function Shell(props) {
Dashboard
- {/*
- Bookings
- */}
+
+
+ Bookings
+
+
Loading...
;
+ }
+
+ return (
+
+
+
Bookings | Calendso
+
+
+
+
+
+
+
+
+
+
+
+ Title
+ |
+
+ Description
+ |
+
+ Name
+ |
+
+ Email
+ |
+
+ Edit
+ |
+
+
+
+ {bookings.map((booking) => (
+
+
+ {booking.title}
+ |
+
+ {booking.description}
+ |
+
+ {booking.attendees[0].name}
+ |
+
+ {booking.attendees[0].email}
+ |
+
+
+ Reschedule
+
+
+ Cancel
+
+ |
+
+ ))}
+
+
+
+
+
+
+
+
+ );
+}
+
+export async function getServerSideProps(context) {
+ const session = await getSession(context);
+
+ if (!session) {
+ return { redirect: { permanent: false, destination: "/auth/login" } };
+ }
+
+ const user = await prisma.user.findFirst({
+ where: {
+ email: session.user.email,
+ },
+ select: {
+ id: true,
+ },
+ });
+
+ const bookings = await prisma.booking.findMany({
+ where: {
+ userId: user.id,
+ },
+ select: {
+ uid: true,
+ title: true,
+ description: true,
+ attendees: true,
+ },
+ });
+
+ return { props: { bookings } };
+}
From ed9245112667868b068aca93d64c9b31a879bf58 Mon Sep 17 00:00:00 2001
From: Alex van Andel
Date: Thu, 24 Jun 2021 14:46:35 +0100
Subject: [PATCH 05/10] The page was being rendered during the async
router.replace call (#306)
* The page was being rendered during the async router.replace call
* Adding a different, slightly smaller fix
---
components/Shell.tsx | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/components/Shell.tsx b/components/Shell.tsx
index 57db5acc..59cb8cf3 100644
--- a/components/Shell.tsx
+++ b/components/Shell.tsx
@@ -32,11 +32,9 @@ export default function Shell(props) {
if (!loading && !session) {
router.replace("/auth/login");
- } else if (loading) {
- return Loading...
;
}
- return (
+ return session ? (
- );
+ ) : null;
}
From ab1298e2caa3fd28118f23bf7daad5430984fdb8 Mon Sep 17 00:00:00 2001
From: femyeda
Date: Thu, 24 Jun 2021 10:59:11 -0500
Subject: [PATCH 06/10] Allow user to reset password
---
lib/emails/buildMessageTemplate.ts | 19 ++
lib/emails/sendMail.ts | 30 +++
package.json | 1 +
pages/api/auth/forgot-password.ts | 77 ++++++
pages/api/auth/reset-password.ts | 60 +++++
pages/auth/forgot-password/[id].tsx | 231 ++++++++++++++++++
pages/auth/forgot-password/index.tsx | 153 ++++++++++++
pages/auth/login.tsx | 112 +++++----
prisma/schema.prisma | 8 +
.../messaging/forgot-password.ts | 20 ++
yarn.lock | 5 +
11 files changed, 672 insertions(+), 44 deletions(-)
create mode 100644 lib/emails/buildMessageTemplate.ts
create mode 100644 lib/emails/sendMail.ts
create mode 100644 pages/api/auth/forgot-password.ts
create mode 100644 pages/api/auth/reset-password.ts
create mode 100644 pages/auth/forgot-password/[id].tsx
create mode 100644 pages/auth/forgot-password/index.tsx
create mode 100644 src/forgot-password/messaging/forgot-password.ts
diff --git a/lib/emails/buildMessageTemplate.ts b/lib/emails/buildMessageTemplate.ts
new file mode 100644
index 00000000..2d3f0696
--- /dev/null
+++ b/lib/emails/buildMessageTemplate.ts
@@ -0,0 +1,19 @@
+import Handlebars from "handlebars";
+
+export const buildMessageTemplate = ({
+ messageTemplate,
+ subjectTemplate,
+ vars,
+}): { subject: string; message: string } => {
+ const buildMessage = Handlebars.compile(messageTemplate);
+ const message = buildMessage(vars);
+
+ const buildSubject = Handlebars.compile(subjectTemplate);
+ const subject = buildSubject(vars);
+ return {
+ subject,
+ message,
+ };
+};
+
+export default buildMessageTemplate;
diff --git a/lib/emails/sendMail.ts b/lib/emails/sendMail.ts
new file mode 100644
index 00000000..917a7308
--- /dev/null
+++ b/lib/emails/sendMail.ts
@@ -0,0 +1,30 @@
+import { serverConfig } from "../serverConfig";
+import nodemailer, { SentMessageInfo } from "nodemailer";
+
+const sendEmail = ({ to, subject, text, html = null }): Promise =>
+ new Promise((resolve, reject) => {
+ const { transport, from } = serverConfig;
+
+ if (!to || !subject || (!text && !html)) {
+ return reject("Missing required elements to send email.");
+ }
+
+ nodemailer.createTransport(transport).sendMail(
+ {
+ from: `Calendso ${from}`,
+ to,
+ subject,
+ text,
+ html,
+ },
+ (error, info) => {
+ if (error) {
+ console.error("SEND_INVITATION_NOTIFICATION_ERROR", to, error);
+ return reject(error.message);
+ }
+ return resolve(info);
+ }
+ );
+ });
+
+export default sendEmail;
diff --git a/package.json b/package.json
index 2bec1c87..c59a83a0 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"googleapis": "^67.1.1",
"handlebars": "^4.7.7",
"ics": "^2.27.0",
+ "lodash.debounce": "^4.0.8",
"lodash.merge": "^4.6.2",
"next": "^10.2.0",
"next-auth": "^3.13.2",
diff --git a/pages/api/auth/forgot-password.ts b/pages/api/auth/forgot-password.ts
new file mode 100644
index 00000000..eb5ceb0d
--- /dev/null
+++ b/pages/api/auth/forgot-password.ts
@@ -0,0 +1,77 @@
+import { NextApiRequest, NextApiResponse } from "next";
+import prisma from "../../../lib/prisma";
+import dayjs from "dayjs";
+import { User, ResetPasswordRequest } from "@prisma/client";
+import sendEmail from "../../../lib/emails/sendMail";
+import { buildForgotPasswordMessage } from "../../../src/forgot-password/messaging/forgot-password";
+import timezone from "dayjs/plugin/timezone";
+import utc from "dayjs/plugin/utc";
+dayjs.extend(utc);
+dayjs.extend(timezone);
+
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ if (req.method !== "POST") {
+ return res.status(400).json({ message: "" });
+ }
+
+ try {
+ const rawEmail = req.body?.email;
+
+ const maybeUser: User = await prisma.user.findUnique({
+ where: {
+ email: rawEmail,
+ },
+ select: {
+ name: true,
+ },
+ });
+
+ if (!maybeUser) {
+ return res.status(400).json({ message: "Couldn't find an account for this email" });
+ }
+
+ const now = dayjs().toDate();
+ const maybePreviousRequest = await prisma.resetPasswordRequest.findMany({
+ where: {
+ email: rawEmail,
+ expires: {
+ gt: now,
+ },
+ },
+ });
+
+ let passwordRequest: ResetPasswordRequest;
+
+ if (maybePreviousRequest && maybePreviousRequest?.length >= 1) {
+ passwordRequest = maybePreviousRequest[0];
+ } else {
+ const expiry = dayjs().tz(maybeUser.timeZone).add(6, "hours").toDate();
+ const createdResetPasswordRequest = await prisma.resetPasswordRequest.create({
+ data: {
+ email: rawEmail,
+ expires: expiry,
+ },
+ });
+ passwordRequest = createdResetPasswordRequest;
+ }
+
+ const passwordResetLink = `${process.env.BASE_URL}/auth/reset-password/${passwordRequest.id}`;
+ const { subject, message } = buildForgotPasswordMessage({
+ user: {
+ name: maybeUser.name,
+ },
+ link: passwordResetLink,
+ });
+
+ await sendEmail({
+ to: rawEmail,
+ subject: subject,
+ text: message,
+ });
+
+ return res.status(201).json({ message: "Reset Requested", data: passwordRequest });
+ } catch (reason) {
+ console.error(reason);
+ return res.status(500).json({ message: "Unable to create password reset request" });
+ }
+}
diff --git a/pages/api/auth/reset-password.ts b/pages/api/auth/reset-password.ts
new file mode 100644
index 00000000..f43b93ca
--- /dev/null
+++ b/pages/api/auth/reset-password.ts
@@ -0,0 +1,60 @@
+import { NextApiRequest, NextApiResponse } from "next";
+import prisma from "../../../lib/prisma";
+import dayjs from "dayjs";
+import { User, ResetPasswordRequest } from "@prisma/client";
+import timezone from "dayjs/plugin/timezone";
+import utc from "dayjs/plugin/utc";
+dayjs.extend(utc);
+dayjs.extend(timezone);
+import { hashPassword } from "../../../lib/auth";
+
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ if (req.method !== "POST") {
+ return res.status(400).json({ message: "" });
+ }
+
+ try {
+ const rawPassword = req.body?.password;
+ const rawRequestId = req.body?.requestId;
+
+ if (!rawPassword || !rawRequestId) {
+ return res.status(400).json({ message: "Couldn't find an account for this email" });
+ }
+
+ const maybeRequest: ResetPasswordRequest = await prisma.resetPasswordRequest.findUnique({
+ where: {
+ id: rawRequestId,
+ },
+ });
+
+ if (!maybeRequest) {
+ return res.status(400).json({ message: "Couldn't find an account for this email" });
+ }
+
+ const maybeUser: User = await prisma.user.findUnique({
+ where: {
+ email: maybeRequest.email,
+ },
+ });
+
+ if (!maybeUser) {
+ return res.status(400).json({ message: "Couldn't find an account for this email" });
+ }
+
+ const hashedPassword = await hashPassword(rawPassword);
+
+ await prisma.user.update({
+ where: {
+ id: maybeUser.id,
+ },
+ data: {
+ password: hashedPassword,
+ },
+ });
+
+ return res.status(201).json({ message: "Password reset." });
+ } catch (reason) {
+ console.error(reason);
+ return res.status(500).json({ message: "Unable to create password reset request" });
+ }
+}
diff --git a/pages/auth/forgot-password/[id].tsx b/pages/auth/forgot-password/[id].tsx
new file mode 100644
index 00000000..48c5824b
--- /dev/null
+++ b/pages/auth/forgot-password/[id].tsx
@@ -0,0 +1,231 @@
+import { getCsrfToken } from "next-auth/client";
+import prisma from "../../../lib/prisma";
+
+import Head from "next/head";
+import React from "react";
+import debounce from "lodash.debounce";
+import dayjs from "dayjs";
+import { ResetPasswordRequest } from "@prisma/client";
+import { useMemo } from "react";
+import Link from "next/link";
+import { GetServerSidePropsContext } from "next";
+
+type Props = {
+ id: string;
+ resetPasswordRequest: ResetPasswordRequest;
+ csrfToken: string;
+};
+
+export default function Page({ resetPasswordRequest, csrfToken }: Props) {
+ const [loading, setLoading] = React.useState(false);
+ const [error, setError] = React.useState(null);
+ const [success, setSuccess] = React.useState(false);
+
+ const [password, setPassword] = React.useState("");
+ const handleChange = (e) => {
+ setPassword(e.target.value);
+ };
+
+ const submitChangePassword = async ({ password, requestId }) => {
+ try {
+ const res = await fetch("/api/auth/reset-password", {
+ method: "POST",
+ body: JSON.stringify({ requestId: requestId, password: password }),
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const json = await res.json();
+
+ if (!res.ok) {
+ setError(json);
+ } else {
+ setSuccess(true);
+ }
+
+ return json;
+ } catch (reason) {
+ setError({ message: "An unexpected error occurred. Try again." });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const debouncedChangePassword = debounce(submitChangePassword, 250);
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ if (!password) {
+ return;
+ }
+
+ if (loading) {
+ return;
+ }
+
+ setLoading(true);
+ setError(null);
+ setSuccess(false);
+
+ await debouncedChangePassword({ password, requestId: resetPasswordRequest.id });
+ };
+
+ const Success = () => {
+ return (
+ <>
+
+
+
Success
+
+
Your password has been reset. You can now login with your newly created password.
+
+
+
+
+ >
+ );
+ };
+
+ const Expired = () => {
+ return (
+ <>
+
+
+
Whoops
+ That Request is Expired.
+
+
+ That request is expired. You can back and enter the email associated with your account and we will
+ you another link to reset your password.
+
+
+
+
+
+ >
+ );
+ };
+
+ const isRequestExpired = useMemo(() => {
+ const now = dayjs();
+ return dayjs(resetPasswordRequest.expires).isBefore(now);
+ }, [resetPasswordRequest]);
+
+ return (
+
+
+
Reset Password
+
+
+
+
+ {isRequestExpired &&
}
+ {!isRequestExpired && !success && (
+ <>
+
+
Reset Password
+
Enter the new password you'd like for your account.
+ {error &&
{error.message}
}
+
+
+ >
+ )}
+ {!isRequestExpired && success && (
+ <>
+
+ >
+ )}
+
+
+
+ );
+}
+
+export async function getServerSideProps(context: GetServerSidePropsContext) {
+ const id = context.params.id;
+
+ try {
+ const resetPasswordRequest = await prisma.resetPasswordRequest.findUnique({
+ where: {
+ id: id,
+ },
+ select: {
+ id: true,
+ expires: true,
+ },
+ });
+
+ return {
+ props: {
+ resetPasswordRequest: {
+ ...resetPasswordRequest,
+ expires: resetPasswordRequest.expires.toString(),
+ },
+ id,
+ csrfToken: await getCsrfToken({ req: context.req }),
+ },
+ };
+ } catch (reason) {
+ return {
+ notFound: true,
+ };
+ }
+}
diff --git a/pages/auth/forgot-password/index.tsx b/pages/auth/forgot-password/index.tsx
new file mode 100644
index 00000000..5760de01
--- /dev/null
+++ b/pages/auth/forgot-password/index.tsx
@@ -0,0 +1,153 @@
+import Head from "next/head";
+import React from "react";
+import { getCsrfToken } from "next-auth/client";
+import debounce from "lodash.debounce";
+
+export default function Page({ csrfToken }) {
+ const [loading, setLoading] = React.useState(false);
+ const [error, setError] = React.useState(null);
+ const [success, setSuccess] = React.useState(false);
+ const [email, setEmail] = React.useState("");
+
+ const handleChange = (e) => {
+ setEmail(e.target.value);
+ };
+
+ const submitForgotPasswordRequest = async ({ email }) => {
+ try {
+ const res = await fetch("/api/auth/forgot-password", {
+ method: "POST",
+ body: JSON.stringify({ email: email }),
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const json = await res.json();
+ if (!res.ok) {
+ setError(json);
+ } else {
+ setSuccess(true);
+ }
+
+ return json;
+ } catch (reason) {
+ setError({ message: "An unexpected error occurred. Try again." });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const debouncedHandleSubmitPasswordRequest = debounce(submitForgotPasswordRequest, 250);
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ if (!email) {
+ return;
+ }
+
+ if (loading) {
+ return;
+ }
+
+ setLoading(true);
+ setError(null);
+ setSuccess(false);
+
+ await debouncedHandleSubmitPasswordRequest({ email });
+ };
+
+ const Success = () => {
+ return (
+
+
Done
+
Check your email. We sent you a link to reset your password.
+ {error &&
{error.message}
}
+
+ );
+ };
+
+ return (
+
+
+
Forgot Password
+
+
+
+
+
+ {success &&
}
+ {!success && (
+ <>
+
+
Forgot Password
+
+ Enter the email address associated with your account and we will send you a link to reset
+ your password.
+
+ {error &&
{error.message}
}
+
+
+ >
+ )}
+
+
+
+ );
+}
+
+Page.getInitialProps = async ({ req }) => {
+ return {
+ csrfToken: await getCsrfToken({ req }),
+ };
+};
diff --git a/pages/auth/login.tsx b/pages/auth/login.tsx
index 72e0c516..76514aa4 100644
--- a/pages/auth/login.tsx
+++ b/pages/auth/login.tsx
@@ -1,55 +1,79 @@
-import Head from 'next/head';
-import { getCsrfToken } from 'next-auth/client';
+import Head from "next/head";
+import Link from "next/link";
+import { getCsrfToken } from "next-auth/client";
export default function Login({ csrfToken }) {
return (
-
-
Login
-
-
-
-
- Sign in to your account
-
-
+
+
Login
+
+
+
+
Sign in to your account
+
-
-
- )
+ );
}
-Login.getInitialProps = async ({ req, res }) => {
+Login.getInitialProps = async ({ req }) => {
return {
- csrfToken: await getCsrfToken({ req })
- }
-}
\ No newline at end of file
+ csrfToken: await getCsrfToken({ req }),
+ };
+};
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 6b33ce75..491fd909 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -142,3 +142,11 @@ model EventTypeCustomInput {
required Boolean
}
+model ResetPasswordRequest {
+ id String @id @default(cuid())
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ email String
+ expires DateTime
+}
+
diff --git a/src/forgot-password/messaging/forgot-password.ts b/src/forgot-password/messaging/forgot-password.ts
new file mode 100644
index 00000000..625d9f60
--- /dev/null
+++ b/src/forgot-password/messaging/forgot-password.ts
@@ -0,0 +1,20 @@
+import buildMessageTemplate from "../../../lib/emails/buildMessageTemplate";
+
+export const forgotPasswordSubjectTemplate = "Forgot your password? - Calendso";
+
+export const forgotPasswordMessageTemplate = `Hey there,
+
+Use the link below to reset your password.
+{{link}}
+
+p.s. It expires in 6 hours.
+
+- Calendso`;
+
+export const buildForgotPasswordMessage = (vars) => {
+ return buildMessageTemplate({
+ subjectTemplate: forgotPasswordSubjectTemplate,
+ messageTemplate: forgotPasswordMessageTemplate,
+ vars,
+ });
+};
diff --git a/yarn.lock b/yarn.lock
index c2f035e9..e5956ac5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2637,6 +2637,11 @@ lodash.clonedeep@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
+lodash.debounce@^4.0.8:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
+ integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168=
+
lodash.includes@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
From 2c29368337c3bdc661ddab04fc0674c53b27a2f6 Mon Sep 17 00:00:00 2001
From: femyeda
Date: Fri, 25 Jun 2021 09:16:24 -0500
Subject: [PATCH 07/10] fix: passwordResetLink uses correct page
---
pages/api/auth/forgot-password.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pages/api/auth/forgot-password.ts b/pages/api/auth/forgot-password.ts
index eb5ceb0d..e856ed42 100644
--- a/pages/api/auth/forgot-password.ts
+++ b/pages/api/auth/forgot-password.ts
@@ -55,7 +55,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
passwordRequest = createdResetPasswordRequest;
}
- const passwordResetLink = `${process.env.BASE_URL}/auth/reset-password/${passwordRequest.id}`;
+ const passwordResetLink = `${process.env.BASE_URL}/auth/forgot-password/${passwordRequest.id}`;
const { subject, message } = buildForgotPasswordMessage({
user: {
name: maybeUser.name,
From ad657c0261bf6e887bdfad737575b17cb7e1f686 Mon Sep 17 00:00:00 2001
From: femyeda
Date: Fri, 25 Jun 2021 09:17:39 -0500
Subject: [PATCH 08/10] use proper response code
---
pages/api/auth/forgot-password.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pages/api/auth/forgot-password.ts b/pages/api/auth/forgot-password.ts
index e856ed42..41596595 100644
--- a/pages/api/auth/forgot-password.ts
+++ b/pages/api/auth/forgot-password.ts
@@ -11,7 +11,7 @@ dayjs.extend(timezone);
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
- return res.status(400).json({ message: "" });
+ return res.status(405).json({ message: "" });
}
try {
From 6fec24a69d7c81cd267989507b06d1819b131179 Mon Sep 17 00:00:00 2001
From: femyeda
Date: Fri, 25 Jun 2021 09:21:21 -0500
Subject: [PATCH 09/10] use lib folder
---
{src => lib}/forgot-password/messaging/forgot-password.ts | 2 +-
pages/api/auth/forgot-password.ts | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
rename {src => lib}/forgot-password/messaging/forgot-password.ts (85%)
diff --git a/src/forgot-password/messaging/forgot-password.ts b/lib/forgot-password/messaging/forgot-password.ts
similarity index 85%
rename from src/forgot-password/messaging/forgot-password.ts
rename to lib/forgot-password/messaging/forgot-password.ts
index 625d9f60..fde5350e 100644
--- a/src/forgot-password/messaging/forgot-password.ts
+++ b/lib/forgot-password/messaging/forgot-password.ts
@@ -1,4 +1,4 @@
-import buildMessageTemplate from "../../../lib/emails/buildMessageTemplate";
+import buildMessageTemplate from "../../emails/buildMessageTemplate";
export const forgotPasswordSubjectTemplate = "Forgot your password? - Calendso";
diff --git a/pages/api/auth/forgot-password.ts b/pages/api/auth/forgot-password.ts
index 41596595..bf4280b8 100644
--- a/pages/api/auth/forgot-password.ts
+++ b/pages/api/auth/forgot-password.ts
@@ -3,7 +3,7 @@ import prisma from "../../../lib/prisma";
import dayjs from "dayjs";
import { User, ResetPasswordRequest } from "@prisma/client";
import sendEmail from "../../../lib/emails/sendMail";
-import { buildForgotPasswordMessage } from "../../../src/forgot-password/messaging/forgot-password";
+import { buildForgotPasswordMessage } from "../../../lib/forgot-password/messaging/forgot-password";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
dayjs.extend(utc);
From e883ab591a361fb7a760cad4bc9ad3d9165ea763 Mon Sep 17 00:00:00 2001
From: femyeda
Date: Fri, 25 Jun 2021 09:23:32 -0500
Subject: [PATCH 10/10] simplify expiry calculation, timezone unneccessary
---
pages/api/auth/forgot-password.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pages/api/auth/forgot-password.ts b/pages/api/auth/forgot-password.ts
index bf4280b8..54f1427a 100644
--- a/pages/api/auth/forgot-password.ts
+++ b/pages/api/auth/forgot-password.ts
@@ -45,7 +45,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (maybePreviousRequest && maybePreviousRequest?.length >= 1) {
passwordRequest = maybePreviousRequest[0];
} else {
- const expiry = dayjs().tz(maybeUser.timeZone).add(6, "hours").toDate();
+ const expiry = dayjs().add(6, "hours").toDate();
const createdResetPasswordRequest = await prisma.resetPasswordRequest.create({
data: {
email: rawEmail,