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 + + + +
+
+
+
+ + + + + + + + + + + + {bookings.map((booking) => ( + + + + + + + + ))} + +
+ Title + + Description + + Name + + Email + + Edit +
+ {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,